Use breadcrumb nav across Horizon

We've added breadcrumbs to the Details pages, but its a pretty
inconsistent experience. This patch adds breadcrumbs to the base
template, making the breadcrumbs consistent across all pages; this will
be useful when the side nav is hidden for responsive design. This patch
also makes the breadcrumbs on the detail pages behave better with
theming.

- Made the detail header and actions avoid using floats
- Moved breadcrumb truncating to the front end, so that it can be
  customised easily via CSS
- Manually added breadcrumb for ngcontainers page
- Removed overly specific HTML check from Key Pair Details test
- Fixed <dt> alignment on Key Pair Details page

Closes-Bug: 1554812
Partially-Implements: blueprint navigation-improvements
Change-Id: Ibcd4c369b5d8ad62f7c839c0deeaefc750677b40
This commit is contained in:
Rob Cresswell 2016-02-19 15:29:10 +00:00 committed by Diana Whitten
parent a55663a49a
commit d5b24d7b97
30 changed files with 128 additions and 100 deletions

View File

@ -0,0 +1,15 @@
{% spaceless %}
<ol class="breadcrumb">
{% for name, target in breadcrumb %}
{% if forloop.last %}
<li class="breadcrumb-item-truncate active">{{ name }}</li>
{% elif target %}
<li class="breadcrumb-item-truncate">
<a href="{{ target }}">{{ name }}</a>
</li>
{% else %}
<li class="breadcrumb-item-truncate">{{ name }}</li>
{% endif %}
{% endfor %}
</ol>
{% endspaceless %}

View File

@ -1,26 +0,0 @@
{% load truncate_filter %}
<ol class="breadcrumb">
{% if breadcrumb %}
{% for name, target in breadcrumb %}
{% if target %}
<li><a href="{{ target }}">{{ name|truncate:20 }}</a></li>
{% else %}
<li class="active">{{ name|truncate:20 }}</li>
{% endif %}
{% endfor %}
{% else %}
{% if panel %}
<li><a href="{{ panel.get_absolute_url }}">{{ panel.name }}</a></li>
{% endif %}
{% endif %}
<li class="active">{{ page_title }}</li>
{% if actions %}
<form class='actions_column pull-right' action='{{ url }}' method="POST">
{% csrf_token %}
{{ actions }}
</form>
{% endif %}
</ol>

View File

@ -1,15 +1,12 @@
{% extends 'base.html' %}
{% load i18n %}
{% load breadcrumb_nav %}
{% block title %}
{{ page_title }}
{% endblock %}
{% block page_header %}
<div class='page-header'>
{% breadcrumb_nav %}
</div>
{% include 'horizon/common/_detail_header.html' %}
{% endblock %}
{% block main %}

View File

@ -0,0 +1,16 @@
<div class='page-header'>
<div class="row">
<div class="col-xs-12 col-sm-9 text-left">
<span class="h1">{{ page_title }}</span>
</div>
<div class="col-xs-12 col-sm-3 text-right">
{% if actions %}
<div class='actions_column' action='{{ url }}' method="POST">
{% csrf_token %}
{{ actions }}
</div>
{% endif %}
</div>
</div>
</div>

View File

@ -1,4 +1,4 @@
# Copyright 2015 Cisco Systems, Inc.
# Copyright 2016 Cisco Systems, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
@ -17,11 +17,44 @@ from django import template
register = template.Library()
@register.inclusion_tag('horizon/common/_breadcrumb_nav.html',
@register.inclusion_tag('bootstrap/breadcrumb.html',
takes_context=True)
def breadcrumb_nav(context):
return {'actions': context.get('actions'),
'breadcrumb': context.get('custom_breadcrumb'),
'url': context.get('url'),
'page_title': context['page_title'],
'panel': context.request.horizon['panel'], }
"""A logic heavy function for automagically creating a breadcrumb.
It uses the dashboard, panel group(if it exists), then current panel.
Can also use a "custom_breadcrumb" context item to add extra items.
"""
breadcrumb = []
dashboard = context.request.horizon['dashboard']
try:
panel_groups = dashboard.get_panel_groups()
except KeyError:
panel_groups = None
panel_group = None
panel = context.request.horizon['panel']
# Add panel group, if there is one
if panel_groups:
for group in panel_groups.values():
if panel.slug in group.panels and group.slug != 'default':
panel_group = group
break
# Remove panel reference if that is the current page
if panel.get_absolute_url() == context.request.path:
panel = None
# Get custom breadcrumb, if there is one.
custom_breadcrumb = context.get('custom_breadcrumb')
# Build list of tuples (name, optional url)
breadcrumb.append((dashboard.name,))
if panel_group:
breadcrumb.append((panel_group.name,))
if panel:
breadcrumb.append((panel.name, panel.get_absolute_url()))
if custom_breadcrumb:
breadcrumb.extend(custom_breadcrumb)
breadcrumb.append((context.get('page_title'),))
return {'breadcrumb': breadcrumb}

View File

@ -1,12 +1,9 @@
{% extends 'base.html' %}
{% load i18n %}
{% load breadcrumb_nav %}
{% block title %}{% trans "Hypervisor Servers" %}{% endblock %}
{% block page_header %}
<div class='page-header'>
{% breadcrumb_nav %}
</div>
{% include "horizon/common/_detail_header.html" %}
{% endblock %}
{% block main %}

View File

@ -12,7 +12,6 @@
# License for the specific language governing permissions and limitations
# under the License.
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
@ -69,7 +68,6 @@ class AdminDetailView(tables.DataTableView):
context = super(AdminDetailView, self).get_context_data(**kwargs)
hypervisor_name = self.kwargs['hypervisor'].split('_', 1)[1]
breadcrumb = [
(_("Hypervisors"), reverse('horizon:admin:hypervisors:index')),
(hypervisor_name,), ]
context['custom_breadcrumb'] = breadcrumb
return context

View File

@ -1,11 +1,9 @@
{% extends 'base.html' %}
{% load i18n breadcrumb_nav %}
{% load i18n %}
{% block title %}{% trans "Volume Type Encryption Details" %}{% endblock %}
{% block page_header %}
<div class='page-header'>
{% breadcrumb_nav %}
</div>
{% include "horizon/common/_detail_header.html" %}
{% endblock %}
{% block main %}

View File

@ -1,13 +1,10 @@
{% extends 'base.html' %}
{% load i18n %}
{% load breadcrumb_nav %}
{% block title %}{% trans "Project Details" %}{% endblock %}
{% block page_header %}
<div class='page-header'>
{% breadcrumb_nav %}
</div>
{% include "horizon/common/_detail_header.html" %}
{% endblock %}
{% block main %}

View File

@ -1,13 +1,10 @@
{% extends 'base.html' %}
{% load i18n %}
{% load breadcrumb_nav %}
{% block title %}{% trans "User Details" %}{% endblock %}
{% block page_header %}
<div class='page-header'>
{% breadcrumb_nav %}
</div>
{% include "horizon/common/_detail_header.html" %}
{% endblock %}
{% block main %}

View File

@ -116,10 +116,6 @@ class KeyPairViewTests(test.TestCase):
url = reverse('horizon:project:access_and_security:keypairs:detail',
kwargs={'keypair_name': keypair.name})
res = self.client.get(url, context)
# Note(Itxaka): With breadcrumbs, the title is in a list as active
self.assertContains(res, '<li class="active">Key Pair Details</li>',
1, 200)
self.assertContains(res, "<dd>%s</dd>" % keypair.name, 1, 200)
@test.create_stubs({api.nova: ("keypair_create", "keypair_delete")})

View File

@ -1,15 +1,14 @@
{% extends 'base.html' %}
{% load i18n sizeformat breadcrumb_nav %}
{% load i18n sizeformat %}
{% block title %}{% trans "Key Pair Details" %}{% endblock %}
{% block page_header %}
<div class='page-header'>
{% breadcrumb_nav %}
</div>
{% include "horizon/common/_detail_header.html" %}
{% endblock %}
{% block main %}
<div class="detail">
<dl class="dl-horizontal">
<dt>{% trans "Name" %}</dt>
<dd>{{ keypair.name|default:_("None") }}</dd>
@ -28,4 +27,5 @@
<div class="key-text word-wrap">{{ keypair.public_key|default:_("None") }}</div>
</dd>
</dl>
</div>
{% endblock %}

View File

@ -1,12 +1,10 @@
{% extends 'base.html' %}
{% load i18n breadcrumb_nav %}
{% load i18n %}
{% block title %}{% trans "Manage Security Group Rules" %}{% endblock %}
{% block page_header %}
<div class='page-header'>
{% breadcrumb_nav %}
</div>
{% include "horizon/common/_detail_header.html" %}
{% endblock %}
{% block main %}

View File

@ -2,6 +2,14 @@
{% load i18n %}
{% block title %}{% trans "Containers" %}{% endblock %}
{% block breadcrumb_nav %}
<ol class="breadcrumb">
<li>{% trans "Project" %}</li>
<li>{% trans "Object Store" %}</li>
<li class="active">{% trans "Containers" %}</li>
</ol>
{% endblock %}
{% block ng_route_base %}
<base href="{{ WEBROOT }}">
{% endblock %}

View File

@ -90,8 +90,6 @@ class RuleDetailsView(tabs.TabView):
rule = self.get_data()
table = fw_tabs.RulesTable(self.request)
breadcrumb = [
(_("Firewalls"),
reverse_lazy('horizon:project:firewalls:firewalls')),
(_("Rules"), reverse_lazy('horizon:project:firewalls:rules'))]
context["custom_breadcrumb"] = breadcrumb
context["rule"] = rule
@ -126,8 +124,6 @@ class PolicyDetailsView(tabs.TabView):
policy = self.get_data()
table = fw_tabs.PoliciesTable(self.request)
breadcrumb = [
(_("Firewalls"),
reverse_lazy('horizon:project:firewalls:firewalls')),
(_("Policies"),
reverse_lazy('horizon:project:firewalls:policies'))]
context["custom_breadcrumb"] = breadcrumb

View File

@ -136,7 +136,6 @@ class VipDetailsView(tabs.TabView):
context['vip'] = vip
vip_nav = vip.pool.name_or_id
breadcrumb = [
(_("Load Balancers"), self.get_redirect_url()),
(vip_nav,
reverse('horizon:project:loadbalancers:vipdetails',
args=(vip.id,))),
@ -173,7 +172,6 @@ class MemberDetailsView(tabs.TabView):
context['member'] = member
member_nav = member.pool.name_or_id
breadcrumb = [
(_("Load Balancers"), self.get_redirect_url()),
(member_nav,
reverse('horizon:project:loadbalancers:pooldetails',
args=(member.pool.id,))),
@ -213,7 +211,6 @@ class MonitorDetailsView(tabs.TabView):
monitor = self.get_data()
context['monitor'] = monitor
breadcrumb = [
(_("Load Balancers"), self.get_redirect_url()),
(_("Monitors"), reverse('horizon:project:loadbalancers:monitors')),
]
context["custom_breadcrumb"] = breadcrumb

View File

@ -89,7 +89,6 @@ class DetailView(tabs.TabView):
network_id=port.network_id)
# TODO(robcresswell) Add URL for "Ports" crumb after bug/1416838
breadcrumb = [
(_("Networks"), self.get_redirect_url()),
((port.network_name or port.network_id), port.network_url),
(_("Ports"),), ]
context["custom_breadcrumb"] = breadcrumb

View File

@ -158,7 +158,6 @@ class DetailView(tabs.TabView):
network_id=subnet.network_id)
# TODO(robcresswell) Add URL for "Subnets" crumb after bug/1416838
breadcrumb = [
(_("Networks"), self.get_redirect_url()),
(network_nav, subnet.network_url),
(_("Subnets"),), ]
context["custom_breadcrumb"] = breadcrumb

View File

@ -1,11 +1,9 @@
{% extends 'base.html' %}
{% load i18n breadcrumb_nav %}
{% load i18n %}
{% block title %}{% trans "Network Details"%}{% endblock %}
{% block page_header %}
<div class='page-header'>
{% breadcrumb_nav %}
</div>
{% include "horizon/common/_detail_header.html" %}
{% endblock %}
{% block main %}

View File

@ -1,11 +1,9 @@
{% extends 'base.html' %}
{% load i18n breadcrumb_nav %}
{% load i18n %}
{% block title %}{% trans "Volume Encryption Details" %}{% endblock %}
{% block page_header %}
<div class='page-header'>
{% breadcrumb_nav %}
</div>
{% include "horizon/common/_detail_header.html" %}
{% endblock %}
{% block main %}

View File

@ -1273,7 +1273,7 @@ class VolumeViewTests(test.TestCase):
self.assertContains(res,
"Volume Encryption Details: %s" % volume.name,
1, 200)
2, 200)
self.assertContains(res, "<dd>%s</dd>" % volume.volume_type, 1, 200)
self.assertContains(res, "<dd>%s</dd>" % enc_meta.provider, 1, 200)
self.assertContains(res, "<dd>%s</dd>" % enc_meta.control_location, 1,
@ -1301,7 +1301,7 @@ class VolumeViewTests(test.TestCase):
self.assertContains(res,
"Volume Encryption Details: %s" % volume.name,
1, 200)
2, 200)
self.assertContains(res, "<h3>Volume is Unencrypted</h3>", 1, 200)
self.assertNoMessages()

View File

@ -76,3 +76,6 @@ $members-list-roles-width: 125px !default;
// https://github.com/twbs/bootstrap/issues/13443
$dropdown-item-padding-vertical: 3px;
$dropdown-item-padding-horizontal: 20px;
// This defines the max-width for a breadcrumb item before it will be truncated
$breadcrumb-item-width: 15em !default;

View File

@ -0,0 +1,9 @@
.breadcrumb {
margin-top: $line-height-computed;
.breadcrumb-item-truncate {
@include text-overflow();
vertical-align: middle;
max-width: $breadcrumb-item-width;
}
}

View File

@ -16,6 +16,7 @@
// Dashboard Components
@import "components/bar_charts";
@import "components/breadcrumbs";
@import "components/charts";
@import "components/checkboxes";
@import "components/datepicker";

View File

@ -1,5 +1,7 @@
{% load branding i18n %}
{% load context_selection %}
{% load breadcrumb_nav %}
<!DOCTYPE html>
<html>
<head>
@ -36,6 +38,10 @@
<div class='container-fluid'>
<div class="row">
<div class="col-xs-12">
{% block breadcrumb_nav %}
{% breadcrumb_nav %}
{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=page_title %}
{% endblock %}

View File

@ -772,9 +772,9 @@ $badge-border-radius: 10px !default;
//##
$breadcrumb-padding-vertical: 8px !default;
$breadcrumb-padding-horizontal: 10px !default;
$breadcrumb-padding-horizontal: 15px !default;
//** Breadcrumb background color
$breadcrumb-bg: $body-bg !default;
$breadcrumb-bg: #f5f5f5 !default;
//** Breadcrumb text color
$breadcrumb-color: $gray !default;
//** Text color of current page in the breadcrumb

View File

@ -1,5 +1,4 @@
.page-header {
border-bottom: 0;
margin: 0;
padding: 0;
}

View File

@ -1,6 +1,5 @@
@import "/bootstrap/scss/bootstrap/mixins/_vendor-prefixes.scss";
@import "components/breadcrumb_header";
@import "components/context_selection";
@import "components/login";
@import "components/messages";

View File

@ -1,10 +0,0 @@
/* Breadcrumb used as a header in the details pages */
.page-header > .breadcrumb {
font-size: $font-size-h3;
margin-bottom: 0;
padding: 8px 0px;
.actions_column {
padding: 0;
}
}

View File

@ -0,0 +1,10 @@
---
features:
- >
[`blueprint navigation-improvements <https://blueprints.launchpad.net/horizon/+spec/navigation-improvements>`_] Breadcrumb navigation has been added across Horizon.
upgrade:
- The breadcrumb navigation inside the details pages now applies across
Horizon. A small change in the logic means that ``custom_breadcrumb``
items in the context no longer need to specify the panel name and link.
See [`blueprint navigation-improvements <https://blueprints.launchpad.net/horizon/+spec/navigation-improvements>`_]