Handle standard menu items defined by Nodes in the browser.

The File menu now includes a "Create" submenu, and Delete/Rename
options. Nodes can offer Delete/Rename functionality, and the
options on the menu are automatically enabled/disabled based on
the selected node. Each node can also offer Create functionality,
and specify a list of node types (including itself) from which the
option should be made available. The menu is dynamically generated
based on the selected node.

The Context menu on the treeview works in a similar way, except that
nodes can offer any context menu items (we don't allow this on the
top menu, as that should stay consistent to avoid user confusion).
This commit is contained in:
Dave Page 2015-03-13 10:35:12 +00:00
parent af7dcd49b4
commit 751f8383fa
13 changed files with 314 additions and 83 deletions

View File

@ -146,6 +146,16 @@ The hook points currently defined for nodes are:
def get_context_menu_items():
"""Return a (set) of dicts of content menu items with name, label, priority and JS"""
def get_create_menu_items():
"""Return a (set) of dicts of create menu items, with a Javascript array of
object types on which the option should appear, name, label. priority and
the function name (no parens) to call on click."""
def get_standard_menu_items():
"""Return a (set) of dicts of standard menu items (drop/rename), with
object type, action, priority and the function name (no parens) to call
on click."""
def get_script_snippets():
"""Return the script snippets needed to handle treeview node operations."""

View File

@ -10,6 +10,7 @@
"""Browser integration functions for the About module."""
from flask import render_template, url_for
from flask.ext.babel import gettext
import config
@ -17,7 +18,7 @@ def get_help_menu_items():
"""Return a (set) of dicts of help menu items, with name, priority, URL,
target and onclick code."""
return [{'name': 'mnu_about',
'label': 'About %s' % (config.APP_NAME),
'label': gettext('About %(appname)s', appname=config.APP_NAME),
'priority': 999,
'url': "#",
'onclick': "about_show()"}]

View File

@ -35,20 +35,28 @@ def get_nodes():
return value
def get_file_menu_items():
"""Return a (set) of dicts of file menu items, with name, priority, URL,
target and onclick code."""
def get_standard_menu_items():
"""Return a (set) of dicts of standard menu items (create/drop/rename), with
object type, action and the function name (no parens) to call on click."""
return [
{'name': 'mnu_add_server_group', 'label': gettext('Add a server group...'), 'priority': 10, 'url': '#', 'onclick': 'add_server_group()'},
{'name': 'mnu_delete_server_group', 'label': gettext('Delete server group'), 'priority': 20, 'url': '#', 'onclick': 'delete_server_group()'},
{'name': 'mnu_rename_server_group', 'label': gettext('Rename server group...'), 'priority': 30, 'url': '#', 'onclick': 'rename_server_group()'}
{'type': 'server-group', 'action': 'drop', 'priority': 10, 'function': 'drop_server_group'},
{'type': 'server-group', 'action': 'rename', 'priority': 20, 'function': 'rename_server_group'}
]
def get_create_menu_items():
"""Return a (set) of dicts of create menu items, with a Javascript array of
object types on which the option should appear, name, label and the function
name (no parens) to call on click."""
return [
{'type': "['server-group']", 'name': 'create_server_group', 'label': gettext('Server Group...'), 'priority': 10, 'function': 'create_server_group'}
]
def get_context_menu_items():
"""Return a (set) of dicts of content menu items with name, node type, label, priority and JS"""
return [
{'name': 'delete_server_group', 'type': NODE_TYPE, 'label': gettext('Delete server group'), 'priority': 10, 'onclick': 'delete_server_group(item);'},
{'name': 'delete_server_group', 'type': NODE_TYPE, 'label': gettext('Delete server group'), 'priority': 10, 'onclick': 'drop_server_group(item);'},
{'name': 'rename_server_group', 'type': NODE_TYPE, 'label': gettext('Rename server group...'), 'priority': 20, 'onclick': 'rename_server_group(item);'}
]

View File

@ -28,20 +28,28 @@ def get_nodes(server_group):
return value
def get_file_menu_items():
"""Return a (set) of dicts of file menu items, with name, priority, URL,
target and onclick code."""
def get_standard_menu_items():
"""Return a (set) of dicts of standard menu items (create/drop/rename), with
object type, action, priority and the function to call on click."""
return [
{'name': 'mnu_add_server', 'label': gettext('Add a server...'), 'priority': 50, 'url': '#', 'onclick': 'add_server()'},
{'name': 'mnu_delete_server', 'label': gettext('Delete server'), 'priority': 60, 'url': '#', 'onclick': 'delete_server()'},
{'name': 'mnu_rename_server', 'label': gettext('Rename server...'), 'priority': 70, 'url': '#', 'onclick': 'rename_server()'}
{'type': 'server', 'action': 'drop', 'priority': 50, 'function': 'drop_server'},
{'type': 'server', 'action': 'rename', 'priority': 60, 'function': 'rename_server'}
]
def get_create_menu_items():
"""Return a (set) of dicts of create menu items, with a Javascript array of
object types on which the option should appear, name, label, priority and
the function name (no parens) to call on click."""
return [
{'type': "['server-group', 'server']", 'name': 'create_server', 'label': gettext('Server...'), 'priority': 50, 'function': 'create_server'}
]
def get_context_menu_items():
"""Return a (set) of dicts of content menu items with name, node type, label, priority and JS"""
return [
{'name': 'delete_server', 'type': NODE_TYPE, 'label': gettext('Delete server'), 'priority': 50, 'onclick': 'delete_server(item);'},
{'name': 'delete_server', 'type': NODE_TYPE, 'label': gettext('Delete server'), 'priority': 50, 'onclick': 'drop_server(item);'},
{'name': 'rename_server', 'type': NODE_TYPE, 'label': gettext('Rename server...'), 'priority': 60, 'onclick': 'rename_server(item);'}
]

View File

@ -1,7 +1,7 @@
// Add a server
function add_server() {
function create_server() {
var alert = alertify.prompt(
'{{ _('Add a server') }}',
'{{ _('Create a server') }}',
'{{ _('Enter a name for the new server') }}',
'',
function(evt, value) {
@ -32,12 +32,12 @@ function add_server() {
}
// Delete a server
function delete_server(item) {
function drop_server(item) {
alertify.confirm(
'{{ _('Delete server?') }}',
'{{ _('Are you sure you wish to delete the server "{0}"?') }}'.replace('{0}', tree.getLabel(item)),
'{{ _('Drop server?') }}',
'{{ _('Are you sure you wish to drop the server "{0}"?') }}'.replace('{0}', tree.getLabel(item)),
function() {
var id = tree.getId(item)
var id = tree.getId(item).split('/').pop()
$.post("{{ url_for('NODE-server.delete') }}", { id: id })
.done(function(data) {
if (data.success == 0) {
@ -66,7 +66,7 @@ function rename_server(item) {
'{{ _('Enter a new name for the server') }}',
tree.getLabel(item),
function(evt, value) {
var id = tree.getId(item)
var id = tree.getId(item).split('/').pop()
$.post("{{ url_for('NODE-server.rename') }}", { id: id, name: value })
.done(function(data) {
if (data.success == 0) {

View File

@ -1,5 +1,5 @@
// Add a server group
function add_server_group() {
function create_server_group() {
var alert = alertify.prompt(
'{{ _('Add a server group') }}',
'{{ _('Enter a name for the new server group') }}',
@ -32,12 +32,12 @@ function add_server_group() {
}
// Delete a server group
function delete_server_group(item) {
function drop_server_group(item) {
alertify.confirm(
'{{ _('Delete server group?') }}',
'{{ _('Are you sure you wish to delete the server group "{0}"?') }}'.replace('{0}', tree.getLabel(item)),
function() {
var id = tree.getId(item)
var id = tree.getId(item).split('/').pop()
$.post("{{ url_for('NODE-server-group.delete') }}", { id: id })
.done(function(data) {
if (data.success == 0) {
@ -66,7 +66,7 @@ function rename_server_group(item) {
'{{ _('Enter a new name for the server group') }}',
tree.getLabel(item),
function(evt, value) {
var id = tree.getId(item)
var id = tree.getId(item).split('/').pop()
$.post("{{ url_for('NODE-server-group.rename') }}", { id: id, name: value })
.done(function(data) {
if (data.success == 0) {

View File

@ -9,6 +9,7 @@
"""Defines views for management of server groups"""
import traceback
from flask import Blueprint, Response, current_app, request
from flask.ext.babel import gettext
from flask.ext.security import current_user, login_required

View File

@ -17,38 +17,50 @@
<ul class="nav navbar-nav">
{% if file_items is defined and file_items|count > 0 %}<li class="dropdown">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">{{ _('File') }} <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">{% for file_item in file_items %}
<li><a id="{{ file_item.name }}" href="{{ file_item.url }}" target="{{ file_item.target }}" onclick="{{ file_item.onclick|safe }}">{{ file_item.label }}</a></li>{% endfor %}
<ul class="dropdown-menu" role="menu">
<li id="mnu_create_dropdown" class="menu-item dropdown dropdown-submenu">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Create</a>
<ul id="mnu_create" class="dropdown-menu">
<li class="menu-item">
<a href="#">Link 1</a>
</li>
</ul>
</li>
<li><a id="mnu_drop_object" href="#" onclick="drop_object()">{{ _('Drop object') }}</a></li>
<li><a id="mnu_rename_object" href="#" onclick="rename_object()">{{ _('Rename object') }}</a></li>
<li class="divider"></li>
{% if file_items is defined and file_items|count > 0 %}{% for file_item in file_items %}
<li><a id="{{ file_item.name }}" href="{{ file_item.url }}"{% if file_item.target %} target="{{ file_item.target }}"{% endif %}{% if file_item.onclick %} onclick="{{ file_item.onclick|safe }}"{% endif %}>{{ file_item.label }}</a></li>{% endfor %}{% endif %}
</ul>
</li>{% endif %}
</li>
{% if edit_items is defined and edit_items|count > 0 %}<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">{{ _('Edit') }} <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">{% for edit_item in edit_items %}
<li><a id="{{ edit_item.name }}" href="{{ edit_item.url }}" target="{{ edit_item.target }}" onclick="{{ edit_item.onclick|safe }}">{{ edit_item.label }}</a></li>{% endfor %}
<li><a id="{{ edit_item.name }}" href="{{ edit_item.url }}"{% if edit_item.target %} target="{{ edit_item.target }}"{% endif %}{% if edit_item.onclick %} onclick="{{ edit_item.onclick|safe }}"{% endif %}>{{ edit_item.label }}</a></li>{% endfor %}
</ul>
</li>{% endif %}
{% if tools_items is defined and tools_items|count > 0 %}<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">{{ _('Tools') }} <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">{% for tools_item in tools_items %}
<li><a id="{{ tools_item.name }}" href="{{ tools_item.url }}" target="{{ tools_item.target }}" onclick="{{ tools_item.onclick|safe }}">{{ tools_item.label }}</a></li>{% endfor %}
<li><a id="{{ tools_item.name }}" href="{{ tools_item.url }}"{% if tools_item.target %} target="{{ tools_item.target }}"{% endif %}{% if tools_item.onclick %} onclick="{{ tools_item.onclick|safe }}"{% endif %}>{{ tools_item.label }}</a></li>{% endfor %}
</ul>
</li>{% endif %}
{% if management_items is defined and management_items|count > 0 %}<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">{{ _('Management') }} <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">{% for management_item in management_items %}
<li><a id="{{ management_item.name }}" href="{{ management_item.url }}" target="{{ management_item.target }}" onclick="{{ management_item.onclick|safe }}">{{ management_item.label }}</a></li>{% endfor %}
<li><a id="{{ management_item.name }}" href="{{ management_item.url }}"{% if management_item.target %} target="{{ management_item.target }}"{% endif %}{% if management_item.onclick %} onclick="{{ management_item.onclick|safe }}"{% endif %}>{{ management_item.label }}</a></li>{% endfor %}
</ul>
</li>{% endif %}
{% if help_items is defined and help_items|count > 0 %}<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">{{ _('Help') }} <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">{% for help_item in help_items %}
<li><a id="{{ help_item.name }}" href="{{ help_item.url }}" target="{{ help_item.target }}" onclick="{{ help_item.onclick|safe }}">{{ help_item.label }}</a></li>{% endfor %}
<li><a id="{{ help_item.name }}" href="{{ help_item.url }}"{% if help_item.target %} target="{{ help_item.target }}"{% endif %}{% if help_item.onclick %} onclick="{{ help_item.onclick|safe }}"{% endif %}>{{ help_item.label }}</a></li>{% endfor %}
</ul>
</li>{% endif %}

View File

@ -72,6 +72,142 @@ function buildDefaultLayout() {
browserPanel = docker.addPanel('pnl_browser', wcDocker.DOCK_LEFT, browserPanel);
}
function report_error(message, info) {
text = '<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">\
<div class="panel panel-default">\
<div class="panel-heading" role="tab" id="headingOne">\
<h4 class="panel-title">\
<a data-toggle="collapse" data-parent="#accordion" href="#collapseOne" aria-expanded="true" aria-controls="collapseOne">\
{{ _('Error message') }}\
</a>\
</h4>\
</div>\
<div id="collapseOne" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="headingOne">\
<div class="panel-body" style="overflow: scroll;">' + message + '</div>\
</div>\
</div>'
if (info != null && info != '') {
text += '<div class="panel panel-default">\
<div class="panel-heading" role="tab" id="headingTwo">\
<h4 class="panel-title">\
<a class="collapsed" data-toggle="collapse" data-parent="#accordion" href="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">\
{{ _('Additional info') }}\
</a>\
</h4>\
</div>\
<div id="collapseTwo" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingTwo">\
<div class="panel-body" style="overflow: scroll;">' + info + '</div>\
</div>\
</div>\
</div>'
}
text += '</div>'
alertify.alert(
'{{ _('An error has occurred') }}',
text
)
}
// Enable/disable menu options
function enable_disable_menus() {
// Disable everything first
$("#mnu_create").html('<li class="menu-item disabled"><a href="#">{{ _('No object selected') }}</a></li>\n');
$("#mnu_drop_object").addClass("mnu-disabled");
$("#mnu_rename_object").addClass("mnu-disabled");
node_type = get_selected_node_type()
// List the possible standard items, their types and actions
var handlers = [{% if standard_items is defined %}{% for standard_item in standard_items %}
"{{ standard_item.type }}:{{ standard_item.action }}",{% endfor %}{% endif %}
]
// Check if we have a matching action for the object type in the list, and
// if so, enable the menu item
if ($.inArray(node_type + ":drop", handlers) >= 0)
$("#mnu_drop_object").removeClass("mnu-disabled");
if ($.inArray(node_type + ":rename", handlers) >= 0)
$("#mnu_rename_object").removeClass("mnu-disabled");
// List the possibe create items
var creators = [{% if create_items is defined %}{% for create_item in create_items %}
[{{ create_item.type }}, "{{ create_item.name }}", "{{ create_item.label }}", "{{ create_item.function }}"],{% endfor %}{% endif %}
]
// Loop through the list of creators and add links for any that apply to this
// node type to the Create menu's UL element
items = ''
for (i = 0; i < creators.length; ++i) {
if ($.inArray(node_type, creators[i][0]) >= 0) {
items = items + '<li class="menu-item"><a href="#" onclick="' + creators[i][3] + '()">' + creators[i][2] + '</a></li>\n'
}
}
if (items != '')
$("#mnu_create").html(items);
}
// Get the selected treeview item type, or nowt
function get_selected_node_type() {
item = tree.selected()
if (!item || item.length != 1)
return "";
return tree.itemData(tree.selected())._type;
}
// Create a new object of the type currently selected
function create_object() {
node_type = get_selected_node_type()
if (node_type == "")
return;
switch(node_type) {
{% if standard_items is defined %}{% for standard_item in standard_items %}{% if standard_item.action == 'create' %}
case '{{ standard_item.type }}':
{{ standard_item.function }}()
break;
{% endif %}{% endfor %}{% endif %}
}
}
// Drop the selected object
function drop_object() {
node_type = get_selected_node_type()
if (node_type == "")
return;
switch(node_type) {
{% if standard_items is defined %}{% for standard_item in standard_items %}{% if standard_item.action == 'drop' %}
case '{{ standard_item.type }}':
{{ standard_item.function }}(tree.selected())
break;
{% endif %}{% endfor %}{% endif %}
}
}
// Rename the selected object
function rename_object() {
node_type = get_selected_node_type()
if (node_type == "")
return;
switch(node_type) {
{% if standard_items is defined %}{% for standard_item in standard_items %}{% if standard_item.action == 'rename' %}
case '{{ standard_item.type }}':
{{ standard_item.function }}(tree.selected())
break;
{% endif %}{% endfor %}{% endif %}
}
}
// Setup the browser
$(document).ready(function(){
@ -166,15 +302,23 @@ ALTER TABLE tickets_detail \n\
selector: '.aciTreeLine',
build: function(element) {
var item = tree.itemFrom(element);
var menu = {
};
var menu = { };
var createMenu = { };
{% if create_items is defined %}
{% for create_item in create_items %}
if ($.inArray(tree.itemData(item)._type, {{ create_item.type }}) >= 0) {
createMenu['{{ create_item.name }}'] = { name: '{{ create_item.label }}', callback: function() { {{ create_item.function }}() }};
}
{% endfor %}{% endif %}
menu["create"] = { "name": "Create" }
menu["create"]["items"] = createMenu
{% if context_items is defined %}
{% for context_item in context_items %}
if (tree.itemData(item)._type == '{{ context_item.type }}') {
menu['{{ context_item.name }}'] = {
name: '{{ context_item.label }}',
callback: function() { {{ context_item.onclick }} }
};
menu['{{ context_item.name }}'] = { name: '{{ context_item.label }}', callback: function() { {{ context_item.onclick }} }};
}
{% endfor %}{% endif %}
return {
@ -184,45 +328,19 @@ ALTER TABLE tickets_detail \n\
};
}
});
// Treeview event handler
$('#tree').on('acitree', function(event, api, item, eventName, options){
switch (eventName){
case "selected":
enable_disable_menus()
break;
}
});
// Setup the menus
enable_disable_menus()
});
function report_error(message, info) {
text = '<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">\
<div class="panel panel-default">\
<div class="panel-heading" role="tab" id="headingOne">\
<h4 class="panel-title">\
<a data-toggle="collapse" data-parent="#accordion" href="#collapseOne" aria-expanded="true" aria-controls="collapseOne">\
{{ _('Error message') }}\
</a>\
</h4>\
</div>\
<div id="collapseOne" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="headingOne">\
<div class="panel-body" style="overflow: scroll;">' + message + '</div>\
</div>\
</div>'
if (info != '') {
text += '<div class="panel panel-default">\
<div class="panel-heading" role="tab" id="headingTwo">\
<h4 class="panel-title">\
<a class="collapsed" data-toggle="collapse" data-parent="#accordion" href="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">\
{{ _('Additional info') }}\
</a>\
</h4>\
</div>\
<div id="collapseTwo" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingTwo">\
<div class="panel-body" style="overflow: scroll;">' + info + '</div>\
</div>\
</div>\
</div>'
}
text += '</div>'
alertify.alert(
'{{ _('An error has occurred') }}',
text
)
}

View File

@ -136,10 +136,20 @@ def browser_js():
# Load the core browser code first
# Get the context menu items
standard_items = [ ]
create_items = [ ]
context_items = [ ]
panel_items = [ ]
for module in modules_and_nodes:
# Get any standard menu items
if 'hooks' in dir(module) and 'get_standard_menu_items' in dir(module.hooks):
standard_items.extend(module.hooks.get_standard_menu_items())
# Get any create menu items
if 'hooks' in dir(module) and 'get_create_menu_items' in dir(module.hooks):
create_items.extend(module.hooks.get_create_menu_items())
# Get any context menu items
if 'hooks' in dir(module) and 'get_context_menu_items' in dir(module.hooks):
context_items.extend(module.hooks.get_context_menu_items())
@ -148,6 +158,8 @@ def browser_js():
if 'hooks' in dir(module) and 'get_panels' in dir(module.hooks):
panel_items += module.hooks.get_panels()
standard_items = sorted(standard_items, key=lambda k: k['priority'])
create_items = sorted(create_items, key=lambda k: k['priority'])
context_items = sorted(context_items, key=lambda k: k['priority'])
panel_items = sorted(panel_items, key=lambda k: k['priority'])
@ -155,6 +167,8 @@ def browser_js():
snippets += render_template('browser/js/browser.js',
layout = layout,
standard_items = standard_items,
create_items = create_items,
context_items = context_items,
panel_items = panel_items)

View File

@ -11,10 +11,6 @@
MODULE_NAME = 'help'
from flask import Blueprint
from flaskext.gravatar import Gravatar
from flask.ext.security import login_required
from flask.ext.login import current_user
from inspect import getmoduleinfo, getmembers
import config

View File

@ -67,4 +67,62 @@ iframe {
/* Prevent tree items wrapping */
.aciTree .aciTreeItem {
white-space: nowrap !important;
}
/* Disabled menu items */
.mnu-disabled {
color: #999999 !important;
}
/*
* Bootstrap 3 remove submenus as they don't work overly well on Mobile. The
* following CSS adds them back in - for our purposes they actually work fine
* on Mobile and are perfectly responsive
*/
.dropdown-submenu {
position:relative;
}
.dropdown-submenu>.dropdown-menu {
top:0;
left:100%;
margin-top:-6px;
margin-left:-1px;
-webkit-border-radius:0 6px 6px 6px;
-moz-border-radius:0 6px 6px 6px;
border-radius:0 6px 6px 6px;
}
.dropdown-submenu:hover>.dropdown-menu {
display:block;
}
.dropdown-submenu>a:after {
display:block;
content:" ";
float:right;
width:0;
height:0;
border-color:transparent;
border-style:solid;
border-width:5px 0 5px 5px;
border-left-color:#cccccc;
margin-top:5px;
margin-right:-10px;
}
.dropdown-submenu:hover>a:after {
border-left-color:#ffffff;
}
.dropdown-submenu.pull-left {
float:none;
}
.dropdown-submenu.pull-left>.dropdown-menu {
left:-100%;
margin-left:10px;
-webkit-border-radius:6px 0 6px 6px;
-moz-border-radius:6px 0 6px 6px;
border-radius:6px 0 6px 6px;
}

View File

@ -144,6 +144,7 @@
.wcMenuList, .context-menu-list {
border: 1px solid #dddddd;
z-index: 999 !important;
}
.wcMenuItem, .context-menu-item {
@ -201,3 +202,7 @@
.wcLayout, .wcLayout tr, .wcLayout td {
vertical-align: top;
}
.context-menu-submenu:after {
content: '>' !important;
}