Merge "Implementing accordion navigation"

This commit is contained in:
Jenkins 2014-03-04 01:02:02 +00:00 committed by Gerrit Code Review
commit 9056162667
15 changed files with 447 additions and 105 deletions

View File

@ -356,9 +356,12 @@ class Dashboard(Registry, HorizonComponent):
which are not connected to specific panels. Default: ``None``.
.. attribute:: nav
.. method:: nav(context)
Optional boolean to control whether or not this dashboard should
appear in automatically-generated navigation. Default: ``True``.
The ``nav`` attribute can be either boolean value or a callable
which accepts a ``RequestContext`` object as a single argument
to control whether or not this dashboard should appear in
automatically-generated navigation. Default: ``True``.
.. attribute:: supports_tenants

View File

@ -0,0 +1,86 @@
horizon.addInitFunction(function() {
var allPanelGroupBodies = $('.nav_accordion > dd > div > ul');
allPanelGroupBodies.each(function(index, value) {
var activePanels = $(this).find('li > a.active');
if(activePanels.length === 0) {
$(this).slideUp(0);
}
});
// mark the active panel group
var activePanel = $('.nav_accordion > dd > div > ul > li > a.active');
activePanel.closest('div').find('h4').addClass('active');
// dashboard click
$('.nav_accordion > dt').click(function() {
var myDashHeader = $(this);
var myDashWasActive = myDashHeader.hasClass("active");
// mark the active dashboard
var allDashboardHeaders = $('.nav_accordion > dt');
allDashboardHeaders.removeClass("active");
// collapse all dashboard contents
var allDashboardBodies = $('.nav_accordion > dd');
allDashboardBodies.slideUp();
// if the current dashboard was active, leave it collapsed
if(!myDashWasActive) {
myDashHeader.addClass("active");
// expand the active dashboard body
var myDashBody = myDashHeader.next();
myDashBody.slideDown();
var activeDashPanel = myDashBody.find("div > ul > li > a.active");
// if the active panel is not in the expanded dashboard
if (activeDashPanel.length === 0) {
// expand the active panel group
var activePanel = myDashBody.find("div:first > ul");
activePanel.slideDown();
activePanel.closest('div').find("h4").addClass("active");
// collapse the inactive panel groups
var nonActivePanels = myDashBody.find("div:not(:first) > ul");
nonActivePanels.slideUp();
}
// the expanded dashboard contains the active panel
else
{
// collapse the inactive panel groups
activeDashPanel.closest('div').find("h4").addClass("active");
allPanelGroupBodies.each(function(index, value) {
var activePanels = value.find('li > a.active');
if(activePanels.length === 0) {
$(this).slideUp();
}
});
}
}
return false;
});
// panel group click
$('.nav_accordion > dd > div > h4').click(function() {
var myPanelGroupHeader = $(this);
myPanelGroupWasActive = myPanelGroupHeader.hasClass("active");
// collapse all panel groups
var allPanelGroupHeaders = $('.nav_accordion > dd > div > h4');
allPanelGroupHeaders.removeClass("active");
allPanelGroupBodies.slideUp();
// expand the selected panel group if not already active
if(!myPanelGroupWasActive) {
myPanelGroupHeader.addClass("active");
myPanelGroupHeader.closest('div').find('ul').slideDown();
}
});
// panel selection
$('.nav_accordion > dd > ul > li > a').click(function() {
horizon.modals.modal_spinner(gettext("Loading"));
});
});

View File

@ -1,5 +1,6 @@
{% load i18n %}
{% load branding i18n %}
{% load url from future %}
<h1 class="brand"><a href="{% site_branding_link %}">{% site_branding %}</a></h1>
<div id="user_info" class="pull-right">
<span>{% blocktrans with username=request.user.username %}Logged in as: {{ username }}{% endblocktrans %}</span>
{% if HORIZON_CONFIG.help_url %}

View File

@ -16,16 +16,18 @@
<body id="{% block body_id %}{% endblock %}" ng-app='hz'>
{% block content %}
<div id="container">
{% block sidebar %}
{% include 'horizon/common/_sidebar.html' %}
{% endblock %}
<div id='main_content'>
<div class='topbar'>
<div class='topbar'>
{% include "_header.html" %}
{% block page_header %}{% endblock %}
</div>
</div>
<div id='main_content'>
{% include "horizon/_messages.html" %}
{% block main %}{% endblock %}
{% block sidebar %}
{% include 'horizon/common/_sidebar.html' %}
{% endblock %}
<div id='content_body'>
{% block page_header %}{% endblock %}
{% block main %}{% endblock %}
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,40 @@
{% load horizon i18n %}
{% load url from future %}
<div>
<dl class="nav_accordion">
{% for dashboard, panel_info in components %}
{% if user|has_permissions:dashboard %}
{% if dashboard.supports_tenants and request.user.authorized_tenants or not dashboard.supports_tenants %}
<dt {% if current.slug == dashboard.slug %}class="active"{% endif %}>
<div>{{ dashboard.name }}</div>
</dt>
{% if current.slug == dashboard.slug %}
<dd>
{% else %}
<dd style="display:none;">
{% endif %}
{% for heading, panels in panel_info.iteritems %}
{% with panels|has_permissions_on_list:user as filtered_panels %}
{% if filtered_panels %}
{% if heading %}
<div><h4><div>{{ heading }}</div></h4>
{% endif %}
<ul>
{% for panel in filtered_panels %}
<li><a href="{{ panel.get_absolute_url }}" {% if current.slug == dashboard.slug and current_panel == panel.slug %}class="active"{% endif %} >{{ panel.name }}</a></li>
{% endfor %}
</ul>
{% if heading %}
</div>
{% endif %}
{% endif %}
{% endwith %}
{% endfor %}
</dd>
{% endif %}
{% endif %}
{% endfor %}
</dl>
</div>

View File

@ -26,6 +26,7 @@
<script src="{{ STATIC_URL }}horizon/lib/hogan-2.0.0.js" type="text/javascript" charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.accordion_nav.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.communication.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.cookies.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.forms.js' type='text/javascript' charset='utf-8'></script>

View File

@ -2,55 +2,5 @@
{% load url from future %}
<div class='sidebar'>
<h1 class="brand clearfix"><a href="{% site_branding_link %}">{% site_branding %}</a></h1>
{% horizon_main_nav %}
{% if request.horizon.dashboard.supports_tenants %}
<div id="tenant_switcher" class="dropdown switcher_bar" tabindex="1">
{% with num_of_tenants=authorized_tenants|length %}
{% if num_of_tenants > 1 %}
<a class="dropdown-toggle" data-toggle="dropdown" href="#tenant_switcher">
{% endif %}
<h4>{% trans "Current Project" %}</h4>
<h3>{{ request.user.tenant_name }}</h3>
{% if num_of_tenants > 1 %}
</a>
{% endif %}
{% if num_of_tenants > 1 %}
<ul id="tenant_list" class="dropdown-menu">
<li class='divider'></li>
{% with authorized_tenants_sorted=authorized_tenants|dictsort:"name" %}
{% for tenant in authorized_tenants_sorted %}
{% if tenant.enabled and tenant.id != request.user.tenant_id %}
<li><a href="{% url 'switch_tenants' tenant.id %}?next={{ request.horizon.dashboard.get_absolute_url }}">{{ tenant.name }}</a></li>
{% endif %}
{% endfor %}
{% endwith %}
</ul>
{% endif %}
{% endwith %}
</div>
{% endif %}
{% with num_of_regions=request.user.available_services_regions|length %}
{% if num_of_regions > 1 %}
<div id="services_region_switcher" class="dropdown switcher_bar" tabindex="1">
<a class="dropdown-toggle" data-toggle="dropdown" href="#services_region_switcher">
<h4>{% trans "Managing Region" %}</h4>
<h3>{{ request.user.services_region }}</h3>
</a>
<ul id="services_regions_list" class="dropdown-menu">
<li class='divider'></li>
{% for region in request.user.available_services_regions %}
<li><a href="{% url 'switch_services_region' region %}?next={{ request.horizon.panel.get_absolute_url }}">{{ region }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endwith %}
{% horizon_dashboard_nav %}
{% horizon_nav %}
</div>

View File

@ -42,6 +42,36 @@ def has_permissions_on_list(components, user):
in components if has_permissions(user, component)]
@register.inclusion_tag('horizon/_accordion_nav.html', takes_context=True)
def horizon_nav(context):
if 'request' not in context:
return {}
current_dashboard = context['request'].horizon.get('dashboard', None)
current_panel = context['request'].horizon.get('panel', None)
dashboards = []
for dash in Horizon.get_dashboards():
panel_groups = dash.get_panel_groups()
non_empty_groups = []
for group in panel_groups.values():
allowed_panels = []
for panel in group:
if callable(panel.nav) and panel.nav(context):
allowed_panels.append(panel)
elif not callable(panel.nav) and panel.nav:
allowed_panels.append(panel)
if allowed_panels:
non_empty_groups.append((group.name, allowed_panels))
if callable(dash.nav) and dash.nav(context):
dashboards.append((dash, SortedDict(non_empty_groups)))
elif not callable(dash.nav) and dash.nav:
dashboards.append((dash, SortedDict(non_empty_groups)))
return {'components': dashboards,
'user': context['request'].user,
'current': current_dashboard,
'current_panel': current_panel.slug if current_panel else '',
'request': context['request']}
@register.inclusion_tag('horizon/_nav_list.html', takes_context=True)
def horizon_main_nav(context):
"""Generates top-level dashboard navigation entries."""

View File

@ -41,9 +41,7 @@ def openstack(request):
# Auth/Keystone context
context.setdefault('authorized_tenants', [])
current_dash = request.horizon['dashboard']
needs_tenants = getattr(current_dash, 'supports_tenants', False)
if request.user.is_authenticated() and needs_tenants:
if request.user.is_authenticated():
context['authorized_tenants'] = request.user.authorized_tenants
# Region context/support

View File

@ -25,7 +25,11 @@ class Settings(horizon.Dashboard):
slug = "settings"
panels = ('user', 'password', )
default_panel = 'user'
nav = False
def nav(self, context):
if context['request'].horizon.get('dashboard', None).slug == self.slug:
return True
return False
horizon.register(Settings)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

View File

@ -0,0 +1,136 @@
@accordionBorderColor: #e5e5e5;
.outline(@outline) {
outline: @outline;
-moz-outline-style: @outline;
}
.nav_accordion {
background-color: #f9f9f9;
color: #6e6e6e;
margin: 0px 0px;
dt, dd {
padding: 10px 0 10px 0;
line-height: 18px;
h4 {
border: 1px solid #BBBBBB;
border-right: 0;
border-bottom: 0;
background-color: #f0f0f0;
background-repeat: no-repeat;
background-position: 96% center;
background-image: url(/static/dashboard/img/right_droparrow.png);
padding: 10px 0 10px 0;
line-height: 16px;
margin-top: 0;
color: #6e6e6e;
font-weight: bold;
text-rendering: optimizelegibility;
max-width: 193px;
padding-right: 16px;
cursor: pointer;
div {
color: #6e6e6e;
font-size: 14px;
margin: 0 0 0 14px;
display: block;
font-weight: bold;
.outline(none);
overflow: hidden;
text-overflow: ellipsis;
max-width: 177px;
}
}
h4.active {
border-bottom: 1px solid #BBBBBB;
background-image: url(/static/dashboard/img/drop_arrow.png);
}
a {
color: #6e6e6e;
font-size: 16px;
margin: 0 0 0 14px;
padding: 0;
display: block;
font-weight: bold;
.outline(none);
text-decoration: none;
}
ul {
list-style: none outside none;
margin: 10px 0 0;
width: 222px;
}
li {
a {
width: 185px;
padding: 10px;
display: block;
line-height: 18px;
margin-left: 20px;
font-weight: normal;
font-size: 13px;
}
a.active {
background: #fff;
border-top: 2px solid @accordionBorderColor;
border-left: 4px solid #d93c27;
border-bottom: 2px solid @accordionBorderColor;
margin-left: 18px;
.border-radius(5px 0 0 5px);
}
a:last-child {
margin-bottom: 8px;
}
}
}
dd {
padding: 0;
font-size: 12px;
}
dt {
border-top: 1px solid #BBBBBB;
background-color: @accordionBorderColor;
background-repeat: no-repeat;
background-position: 96% center;
background-image: url(/static/dashboard/img/right_droparrow.png);
padding-right: 16px;
max-width: 217px;
cursor: pointer;
div {
color: #6e6e6e;
font-size: 14px;
margin: 0 0 0 14px;
padding: 0;
font-weight: bold;
.outline(none);
overflow: hidden;
text-overflow: ellipsis;
max-width: 201px;
}
}
dt.active {
background-image: url(/static/dashboard/img/drop_arrow.png);
}
dt:first-child {
border-top: 0;
}
dt a {
text-decoration: none;
}
}

View File

@ -1,5 +1,6 @@
@import "../../bootstrap/less/bootstrap.less";
@import "../../bootstrap/less/datepicker.less";
@import "accordion_nav.less";
/* new clearfix */
.clearfix:after {
@ -37,20 +38,11 @@ dt {
font-weight: bold;
}
#main_content {
padding-left: 250px;
padding-right: 25px;
}
.topbar {
background: #f2f2f2;
border-bottom: 1px solid #e5e5e5;
padding: 10px 25px;
margin-top: 0;
margin-left: -25px;
margin-bottom: 20px;
margin-right: -25px;
min-width: 700px;
#gradient > .vertical(#FFFFFF, #d8d8d8);
border-bottom: 1px solid #cccccc;
padding: 10px 0px 15px 0px;
}
.topbar .switcher_bar {
@ -59,17 +51,18 @@ dt {
width: 160px;
background-position: 140px center;
margin-bottom: 0;
font-size: 11px;
margin-left: 20px;
margin-right: 10px;
padding: 0;
background-image: url(/static/dashboard/img/drop_arrow.png);
border: 1px solid #c0d9e4;
background-color: #e9f5fa;
background-color: #eee;
background-repeat: no-repeat;
border: 1px solid #ccc;
border-bottom-color: #bbb;
.border-radius(4px);
.box-shadow(e('inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05)'));
}
.topbar .switcher_bar a {
padding: 2px 10px 1px;
margin-left: 0;
display: block;
}
@ -78,21 +71,60 @@ dt {
width: 130px;
}
.topbar .switcher_bar h3 {
padding: 2px 10px 1px;
font-size: 13px !important;
line-height: 20px;
color: #6e6e6e;
text-decoration: none;
}
.topbar .switcher_bar div {
padding: 2px 10px 1px;
font-size: 11px !important;
line-height: 20px;
color: #6e6e6e;
text-decoration: none;
overflow: hidden;
padding-right: 32px;
text-overflow: ellipsis;
}
.topbar .context-box {
display: inline;
}
#profile_editor_switcher {
width:auto;
max-width: 200px;
vertical-align: top;
white-space: nowrap;
background-image: url(/static/dashboard/img/profile_drop.png);
background-repeat: no-repeat;
background-position: right center;
}
#user_info {
color: #888;
margin: auto 0;
color: #888;
margin: auto 10px;
padding-bottom: 15px;
padding-right: 25px;
}
#user_info > a {
margin-left: 25px;
font-size: 11px !important;
color: #6e6e6e;
margin-left: 15px;
line-height: 20px;
font-size: 13px !important;
}
.page-header {
margin: 0;
padding: 0;
border: 0;
margin: 0 0 5px 0;
padding: 0 0 5px 0;
border-bottom: 2px solid #e5e5e5;
font-family: anivers;
height: auto;
width: 100%;
}
h2 {
@ -163,10 +195,12 @@ body {
}
.sidebar {
background: #edf9ff;
background-color: #f9f9f9;
border-right: 5px solid #e5e5e5;
border-bottom: 5px solid #e5e5e5;
.border-radius(0 0 5px 0);
float: left;
min-width: 231px;
}
.sidebar h4 {
@ -185,20 +219,16 @@ body {
h1.brand {
width: 100%;
margin: 0;
background-color: #f5f5f5;
padding-bottom: 40px;
}
h1.brand a {
background: url(/static/dashboard/img/logo.png) top left no-repeat;
display: block;
float: left;
width: 116px;
height: 123px;
width: 216px;
height: 35px;
text-indent: -9999px;
margin-left: 56px;
margin-top: 15px;
margin-bottom: 25px;
margin-left: 16px;
}
@ -216,6 +246,7 @@ a.current_item:hover {
a.current_item:hover h3, a.current_item:hover h4 {
color: #39738c;
.border-radius(4px)
}
.sidebar .switcher_bar {
@ -335,10 +366,10 @@ a.current_item:hover h3, a.current_item:hover h4 {
padding: 0 0 0 1px;
}
.table-bordered {
border: none;
}
.table_header {
min-height: 35px;
padding: 5px 0;
@ -1312,6 +1343,11 @@ tr.terminated {
border-bottom: 0 none;
}
#content_body {
padding-left: 255px;
padding-right: 25px;
}
.tab_wrapper {
padding-top: 50px;
}

View File

@ -1,11 +1,66 @@
{% load i18n %}
{% load branding i18n %}
{% load url from future %}
<div id="user_info" class="pull-right">
<span>{% blocktrans with username=request.user.username %}Logged in as: {{ username }}{% endblocktrans %}</span>
<a href="{% url 'horizon:settings:user:index' %}">{% trans "Settings" %}</a>
{% if HORIZON_CONFIG.help_url %}
<a href="{{ HORIZON_CONFIG.help_url }}" target="_new">{% trans "Help" %}</a>
<h1 class="brand"><a href="{% site_branding_link %}">{% site_branding %}</a></h1>
<div class="context-box">
<div id="tenant_switcher" class="dropdown switcher_bar" tabindex="1">
{% with num_of_tenants=authorized_tenants|length %}
{% if num_of_tenants > 1 %}
<a class="dropdown-toggle" data-toggle="dropdown" href="#tenant_switcher">
{% endif %}
<h3>{{ request.user.project_name }}</h3>
{% if num_of_tenants > 1 %}
</a>
{% endif %}
{% if num_of_tenants > 1 %}
<ul id="tenant_list" class="dropdown-menu">
<li class='divider'></li>
{% with authorized_tenants_sorted=authorized_tenants|dictsort:"name" %}
{% for tenant in authorized_tenants_sorted %}
{% if tenant.enabled and tenant.id != request.user.tenant_id %}
<li><a href="{% url 'switch_tenants' tenant.id %}?next={{ request.horizon.dashboard.get_absolute_url }}">{{ tenant.name }}</a></li>
{% endif %}
{% endfor %}
{% endwith %}
</ul>
{% endif %}
{% endwith %}
</div>
{% with num_of_regions=request.user.available_services_regions|length %}
{% if num_of_regions > 1 %}
<div id="services_region_switcher" class="dropdown switcher_bar" tabindex="1">
<a class="dropdown-toggle" data-toggle="dropdown" href="#services_region_switcher">
<h3>{{ request.user.services_region }}</h3>
</a>
<ul id="services_regions_list" class="dropdown-menu">
<li class='divider'></li>
{% for region in request.user.available_services_regions %}
<li><a href="{% url 'switch_services_region' region %}?next={{ request.horizon.panel.get_absolute_url }}">{{ region }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endwith %}
</div>
<div id="user_info" class="pull-right">
<div id="profile_editor_switcher" class="dropdown switcher_bar" tabindex='1'>
<a class="dropdown-toggle" data-toggle="dropdown" href="#profile_editor_switcher">
<div>{{ request.user.username }}</div>
</a>
<ul id="editor_list" class="dropdown-menu">
<li class='divider'></li>
<li><a href="{% url 'horizon:settings:user:index' %}">{% trans "Settings" %}</a></li>
{% if HORIZON_CONFIG.help_url %}
<li><a href="{{ HORIZON_CONFIG.help_url }}" target="_new">{% trans "Help" %}</a></li>
{% endif %}
</ul>
</div>
<a href="{% url 'logout' %}">{% trans "Sign Out" %}</a>
{% include "horizon/common/_region_selector.html" %}
</div>