Sidebar now inherits from a Bootstrap Theme

Horizon's sidebar now uses .nav and implements a nav using
.nav-stacked and .nav-pills to support a standard Bootstrap theme.
It was also simplfied to use Bootstrap's built in accordion
functionality and therefore removes all of the javascript required to
duplicate this experience.  There was also a small glitchy experience
with the custom accordion code, this is now solved with Bootstrap.

All highly customized style elements that were specific to the 'default'
theme were moved into the theme folder itself.  Only the styles
necessary to conform standard Bootstrap remain in the dashboard
directory tree.

The sidebar was formerly named an 'accordion-nav', but this name is
coupled with the presentational experience that is implemented within
the sidebar. Therefore, sidebar is more semantic than accordion.
Because it is now simple to override the sidebar with a non-accordion
experience, we should just call it the sidebar going forward.

sidebar-bg.png was removed as it was only limiting the ability to
easily grow the size of the sidebar.  It was providing a box-shadow
which can be done with pure css.

To test and use this functionality, clean your static directory, then
run collectstatic and compress prior to starting your server.

Partially-Implements: blueprint horizon-theme-css-reorg
Partially-Implements: blueprint bootstrap-html-standards
Change-Id: Ic7fcde7dd82e70815dc7fcae04bc9ae9d618d4d8
This commit is contained in:
Diana Whitten 2015-07-31 13:50:13 -07:00
parent 5c8d52c731
commit 35706f5b6b
19 changed files with 273 additions and 295 deletions

View File

@ -1,93 +0,0 @@
horizon.addInitFunction(function() {
var allPanelGroupBodies = $('.nav_accordion > dd > div > ul');
// In case the event was generated by clicking any mouse button,
// the normalized codes are matched according to http://api.jquery.com/event.which/
var MOUSE_LBUTTON_CODE_NORMALIZED = 1;
var MOUSE_WHEEL_CODE_NORMALIZED = 2;
// 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.removeClass('hidden').slideDown();
activePanel.closest('div').find("h4").addClass("active");
// collapse the inactive panel groups
var nonActivePanels = myDashBody.find("div:not(:first) > ul");
nonActivePanels.slideUp();
nonActivePanels.closest('div').find("h4").removeClass("active");
}
// the expanded dashboard contains the active panel
else
{
// collapse the inactive panel groups
activeDashPanel.closest('div').find("h4").addClass("active");
activeDashPanel.closest('ul').removeClass('hidden').slideDown();
allPanelGroupBodies.each(function(index, value) {
var activePanels = $(value).find('li > a.active');
if(activePanels.length === 0) {
$(this).slideUp();
$(this).closest('div').find("h4").removeClass("active");
}
});
}
}
return false;
});
// panel group click
$('.nav_accordion > dd > div > h4').click(function() {
var myPanelGroupHeader = $(this);
var 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').removeClass('hidden')
.slideDown();
}
});
// panel selection
$('.nav_accordion > dd > div > ul > li > a').click(function(ev) {
// NOTE(tsufiev): prevent infinite 'Loading' spinner when opening link
// in the other browser tab with mouse wheel or mouse lbutton + modifier
if ( ev.which !== MOUSE_WHEEL_CODE_NORMALIZED &&
!( ev.which === MOUSE_LBUTTON_CODE_NORMALIZED &&
( ev.shiftKey || ev.ctrlKey || ev.metaKey ) ) ) {
horizon.modals.modal_spinner(gettext("Loading"));
}
});
});

View File

@ -386,4 +386,19 @@ horizon.addInitFunction(horizon.modals.init = function() {
handle: ".modal-header" handle: ".modal-header"
}); });
}); });
// Helper class to show modal spinner on click
// In case the event was generated by clicking any mouse button,
// the normalized codes are matched according to http://api.jquery.com/event.which/
var MOUSE_LBUTTON_CODE_NORMALIZED = 1;
var MOUSE_WHEEL_CODE_NORMALIZED = 2;
$(document).on('click', '.openstack-spin', function(ev) {
// NOTE(tsufiev): prevent infinite 'Loading' spinner when opening link
// in the other browser tab with mouse wheel or mouse lbutton + modifier
if ( ev.which !== MOUSE_WHEEL_CODE_NORMALIZED &&
!( ev.which === MOUSE_LBUTTON_CODE_NORMALIZED &&
( ev.shiftKey || ev.ctrlKey || ev.metaKey ) ) ) {
horizon.modals.modal_spinner(gettext("Loading"));
}
});
}); });

View File

@ -1,40 +0,0 @@
{% load horizon i18n %}
{% load url from future %}
<dl class="nav_accordion">
{% for dashboard, panel_info in components %}
{% if user|has_permissions:dashboard %}
<dt {% if current.slug == dashboard.slug %}class="active"{% endif %}>
{{ dashboard.name }}
<span class="fa pull-right"></span>
</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>
{{ heading }}
<span class="fa pull-right"></span>
</h4>
{% endif %}
<ul{% if heading and heading != current_panel_group %} class="hidden"{% endif %}>
{% 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 %} tabindex="{{ forloop.counter }}" >{{ panel.name }}</a></li>
{% endfor %}
</ul>
{% if heading %}
</div>
{% endif %}
{% endif %}
{% endwith %}
{% endfor %}
</dd>
{% endif %}
{% endfor %}
</dl>

View File

@ -0,0 +1,58 @@
{% load horizon i18n %}
{% load url from future %}
<ul id="sidebar-accordion" class="nav nav-pills nav-stacked">
{% for dashboard, panel_info in components %}
{% if user|has_permissions:dashboard %}
<li class="panel openstack-dashboard{% if current.slug == dashboard.slug %} active{% endif %}">
<a data-toggle="collapse"
data-parent="#sidebar-accordion"
href="#sidebar-accordion-{{ dashboard.slug }}"
{% if current.slug != dashboard.slug %}
class="collapsed"
{% endif %}>
{{ dashboard.name }}
<span class="openstack-toggle pull-right fa"></span>
</a>
<ul id="sidebar-accordion-{{ dashboard.slug }}"
class="nav collapse panel-collapse{% if current.slug == dashboard.slug %} in{% endif %}">
{% for group, panels in panel_info.iteritems %}
{% with panels|has_permissions_on_list:user as filtered_panels %}
{% if filtered_panels %}
{% if group.name %}
<li class="nav-header panel">
<a data-toggle="collapse"
data-parent="#sidebar-accordion-{{ dashboard.slug }}"
href="#sidebar-accordion-{{ dashboard.slug }}-{{ group.slug }}"
{% if current.slug == dashboard.slug and current_panel_group != group.slug %}class="collapsed"
{% elif current.slug != dashboard.slug and forloop.counter0 != 0 %}class="collapsed"{% endif %}>
<span class="nav-header-title">
{{ group.name }}
<span class="openstack-toggle fa pull-right"></span>
</span>
</a>
<ul id="sidebar-accordion-{{ dashboard.slug }}-{{ group.slug }}"
class="nav collapse panel-collapse
{% if current.slug == dashboard.slug and current_panel_group == group.slug %} in
{% elif current.slug != dashboard.slug and forloop.counter0 == 0 %} in{% endif %}">
{% endif %}
{% for panel in filtered_panels %}
<li class="panel openstack-panel{% if current.slug == dashboard.slug and current_panel == panel.slug %} active{% endif %}">
<a class="openstack-spin" href="{{ panel.get_absolute_url }}"
tabindex="{{ forloop.counter }}" >
{{ panel.name }}
</a>
</li>
{% endfor %}
{% if group.name %}
</ul>
</li>
{% endif %}
{% endif %}
{% endwith %}
{% endfor %}
</ul>
</li>
{% endif %}
{% endfor %}
</ul>

View File

@ -1,6 +1,6 @@
{% load branding horizon i18n %} {% load branding horizon i18n %}
{% load url from future %} {% load url from future %}
<div class='sidebar'> <div id='sidebar'>
{% horizon_nav %} {% horizon_nav %}
</div> </div>

View File

@ -44,7 +44,7 @@ def has_permissions_on_list(components, user):
in components if has_permissions(user, component)] in components if has_permissions(user, component)]
@register.inclusion_tag('horizon/_accordion_nav.html', takes_context=True) @register.inclusion_tag('horizon/_sidebar.html', takes_context=True)
def horizon_nav(context): def horizon_nav(context):
if 'request' not in context: if 'request' not in context:
return {} return {}
@ -65,9 +65,9 @@ def horizon_nav(context):
panel.can_access(context)): panel.can_access(context)):
allowed_panels.append(panel) allowed_panels.append(panel)
if panel == current_panel: if panel == current_panel:
current_panel_group = group.name current_panel_group = group.slug
if allowed_panels: if allowed_panels:
non_empty_groups.append((group.name, allowed_panels)) non_empty_groups.append((group, allowed_panels))
if (callable(dash.nav) and dash.nav(context) and if (callable(dash.nav) and dash.nav(context) and
dash.can_access(context)): dash.can_access(context)):
dashboards.append((dash, SortedDict(non_empty_groups))) dashboards.append((dash, SortedDict(non_empty_groups)))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 B

View File

@ -12,7 +12,6 @@ $webroot: "" !default;
$code-font-family: Menlo, Monaco, Consolas, 'Courier New' !default; $code-font-family: Menlo, Monaco, Consolas, 'Courier New' !default;
$main-content-min-width: 900px !default; $main-content-min-width: 900px !default;
$sidebar-background-color: #f9f9f9 !default;
$sidebar-width: 220px !default; $sidebar-width: 220px !default;
$border-color: #dddddd !default; $border-color: #dddddd !default;
$table-bg-odd: $table-bg-accent !default; $table-bg-odd: $table-bg-accent !default;
@ -66,20 +65,6 @@ $fa-font-path: $webroot + "/static/horizon/lib/font-awesome/fonts";
$overview_chart_height: 81px; $overview_chart_height: 81px;
/* Accordion Navigation */
$accordionBaseFontColor: #6e6e6e !default;
$accordionBorderColor: #e5e5e5 !default;
$accordionBoxShadowColor: #c7c7c7 !default;
$accordionHighlightColor: #d93c27 !default;
$accordionHeaderActiveColor: #e3e4e6 !default;
$accordionHeaderBorderColor: #bbbbbb !default;
$accordionItemActiveBgColor: #ffffff !default;
$accordionItemFontColor: #6e6e6e !default;
$accordionSubBorderColor: #c0c1c2 !default;
$accordionSubHeaderFontColor: #6e6e6e !default;
/* Help panel */ /* Help panel */
// theme // theme

View File

@ -1,100 +0,0 @@
@mixin sidebar_toggle_icon {
& > span {
transition: all .3s ease 0s;
@extend .fa-angle-down;
&:before {
font-size: 1em;
line-height: 18px;
vertical-align: middle;
}
}
&.active > span {
-ms-transform: rotate(180deg); /* IE 9 */
-webkit-transform: rotate(180deg); /* Chrome, Safari, Opera */
transform: rotate(180deg);
}
}
.nav_accordion {
color: $accordionBaseFontColor;
margin: 0px 0px;
dt, dd {
line-height: 18px;
h4 {
padding: .5em 0 .3em 0;
margin: 0 1.2em .5em 1.2em;
border-bottom: 3px solid $accordionSubBorderColor;
font-size: $font-size-base;
line-height: $line-height-base;
color: $accordionSubHeaderFontColor;
font-weight: bold;
text-rendering: optimizelegibility;
cursor: pointer;
@include sidebar_toggle_icon();
transition: all .3s ease 0s;
&:hover{
border-color: darken($accordionSubBorderColor, 10%)
}
}
ul {
list-style: none outside none;
margin: 0;
padding: 0;
}
li {
&:not(:first-child) a {
margin-top: .2em;
}
&:not(:last-child) a {
margin-bottom: .2em;
}
a {
color: $accordionItemFontColor;
padding: .7em 1.2em;
outline: none;
text-decoration: none;
display: block;
text-align: right;
transition: all .3s ease 0s;
&.active, &:hover {
color: $accordionHighlightColor;
}
&.active {
background: $accordionItemActiveBgColor;
border-top: 1px solid $accordionBorderColor;
border-left: 4px solid $accordionHighlightColor;
border-bottom: 1px solid $accordionBorderColor;
-webkit-box-shadow: -3px 2px 6px -3px $accordionBoxShadowColor;
box-shadow: -3px 3px 6px -3px $accordionBoxShadowColor;
}
}
}
}
dd {
padding: 0;
border-bottom: 1px solid $accordionHeaderBorderColor;
div {
&:first-child h4 {
margin-top: .5em;
}
&:last-child h4:not(.active) {
margin-bottom: 1em;
}
}
}
dt {
&:first-child { border-top: 1px solid $accordionHeaderBorderColor; }
border-bottom: 1px solid $accordionHeaderBorderColor;
padding: .5em 1.2em;
max-width: 231px;
cursor: pointer;
@include sidebar_toggle_icon();
transition: all .3s ease 0s;
&.active {
background-color: $accordionHeaderActiveColor;
}
&:hover{
background-color: darken($accordionHeaderActiveColor, 3%)
}
}
}

View File

@ -0,0 +1,66 @@
#sidebar {
margin-left: -$sidebar-width;
left: $sidebar-width;
width: $sidebar-width;
position: absolute;
z-index: 0;
// Sets the arrow toggles for each dashboard list
[data-toggle="collapse"] {
.openstack-toggle.fa {
line-height: $line-height-computed;
width: $line-height-computed;
height: $line-height-computed;
text-align: center;
@include transition(transform 0.3s ease 0s);
@extend .fa-chevron-down;
}
&.collapsed {
.openstack-toggle.fa {
@include rotate(270deg);
}
}
}
// Styles for the Dashboard Names
.openstack-dashboard {
& > a {
border-radius: $border-radius-base $border-radius-base 0 0;
}
}
// Styles for the Panel Names
.openstack-panel {
& > a {
text-align: right;
color: $text-color;
}
&.active > a {
color: $brand-primary;
font-weight: bold;
}
}
.panel {
border: none;
border-radius: 0;
@include box-shadow(none);
margin-bottom: 0;
}
}
// Bootstrap 3 removed nav headers, lets add them back
.nav .nav-header > a > .nav-header-title {
font-size: $font-size-base;
font-weight: bold;
text-transform: uppercase;
text-align: center;
width: 100%;
display: inline-block;
&:hover {
background-color: transparent;
}
}

View File

@ -15,7 +15,7 @@
// Dashboard Components // Dashboard Components
@import "splash"; @import "splash";
@import "components/resource_browser"; @import "components/resource_browser";
@import "components/accordion_nav"; @import "components/sidebar";
@import "components/inline_edit"; @import "components/inline_edit";
@import "components/charts"; @import "components/charts";
@import "components/workflow"; @import "components/workflow";
@ -55,13 +55,6 @@
} }
body { body {
background-color: $body-bg;
&:not(#splash) {
background-image: url('../img/sidebar_bg.png');
background-repeat: repeat-y;
background-position: -200px top;
}
color: $text-color;
} }
small { small {
@ -121,33 +114,6 @@ h2 {
margin-top: 190px; margin-top: 190px;
} }
.sidebar {
background-color: $sidebar-background-color;
padding-top: 20px;
margin-left: -$sidebar-width;
left: $sidebar-width;
width: $sidebar-width;
position: absolute;
z-index: 0;
-webkit-box-shadow:inset -3px 2px 6px -2px #C7C7C7, inset -1px 0 0 0 #cccccc;
box-shadow:inset -3px 2px 6px -2px #C7C7C7, inset -1px 0 0 0 #cccccc;
}
.sidebar h4 {
margin-left: 14px;
color: $headings-color;
}
.sidebar .nav-tabs {
margin-top: -34px;
}
.sidebar .nav-tabs li.active a {
background-color: $body-bg;
}
/* Tenant Dropdown */ /* Tenant Dropdown */

View File

@ -28,7 +28,6 @@
<script src='{{ STATIC_URL }}horizon/lib/bootstrap_datepicker/bootstrap-datepicker.js'></script> <script src='{{ STATIC_URL }}horizon/lib/bootstrap_datepicker/bootstrap-datepicker.js'></script>
<script src="{{ STATIC_URL }}horizon/lib/magic_search/magic_search.js"></script> <script src="{{ STATIC_URL }}horizon/lib/magic_search/magic_search.js"></script>
<script src="{{ STATIC_URL }}horizon/lib/hogan.js"></script> <script src="{{ STATIC_URL }}horizon/lib/hogan.js"></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.accordion_nav.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.communication.js'></script> <script src='{{ STATIC_URL }}horizon/js/horizon.communication.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.datepickers.js'></script> <script src='{{ STATIC_URL }}horizon/js/horizon.datepickers.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.forms.js'></script> <script src='{{ STATIC_URL }}horizon/js/horizon.forms.js'></script>

View File

@ -1,2 +1,3 @@
@import "bootstrap/variables"; @import "bootstrap/variables";
@import "horizon/variables"; @import "horizon/variables";
@import "/horizon/lib/font-awesome/scss/variables";

View File

@ -1 +1,2 @@
@import 'components/navs';
@import 'components/navbar'; @import 'components/navbar';

View File

@ -17,8 +17,8 @@ $bootstrap-sass-asset-helper: (twbs-font-path("") != unquote('twbs-font-path("")
$gray-darker: lighten(#000, 13.5%) !default; // #222 $gray-darker: lighten(#000, 13.5%) !default; // #222
$gray-dark: lighten(#000, 20%) !default; // #333 $gray-dark: lighten(#000, 20%) !default; // #333
$gray: lighten(#000, 33.5%) !default; // #555 $gray: #6e6e6e !default;
$gray-light: #CCC !default; $gray-light: #BBB !default;
$gray-lighter: lighten(#000, 93.5%) !default; // #eee $gray-lighter: lighten(#000, 93.5%) !default; // #eee
$brand-primary: #428bca !default; $brand-primary: #428bca !default;
@ -405,8 +405,8 @@ $navbar-inverse-toggle-border-color: #333 !default;
//## //##
//=== Shared nav styles //=== Shared nav styles
$nav-link-padding: 10px 15px !default; $nav-link-padding: 0.5em 1.2em !default;
$nav-link-hover-bg: $gray-lighter !default; $nav-link-hover-bg: #dbdcdf !default;
$nav-disabled-link-color: $gray-light !default; $nav-disabled-link-color: $gray-light !default;
$nav-disabled-link-hover-color: $gray-light !default; $nav-disabled-link-hover-color: $gray-light !default;
@ -426,9 +426,9 @@ $nav-tabs-justified-link-border-color: #ddd !default;
$nav-tabs-justified-active-link-border-color: $body-bg !default; $nav-tabs-justified-active-link-border-color: $body-bg !default;
//== Pills //== Pills
$nav-pills-border-radius: $border-radius-base !default; $nav-pills-border-radius: 0 !default;
$nav-pills-active-link-hover-bg: $component-active-bg !default; $nav-pills-active-link-hover-bg: #e3e4e6 !default;
$nav-pills-active-link-hover-color: $component-active-color !default; $nav-pills-active-link-hover-color: $gray !default;
//== Pagination //== Pagination

View File

@ -0,0 +1,26 @@
.nav-pills.nav-stacked {
a {
color: $gray;
}
& > li {
& > a {
border-top: 1px solid $gray-light;
border-bottom: 1px solid $gray-light;
border-radius: 0;
font-weight: bold;
}
}
& > li + li {
margin-top: 0;
& > a {
border-top: none;
}
}
& > li > .in {
border-bottom: 1px solid $gray-light;
}
}

View File

@ -1,3 +1,7 @@
@import '/bootstrap/scss/bootstrap/mixins/_vendor-prefixes.scss';
@import 'components/sidebar';
.navbar-brand { .navbar-brand {
// The 114 is a legacy value to push the context-menu over // The 114 is a legacy value to push the context-menu over

View File

@ -0,0 +1,6 @@
$sidebar-box-shadow-color: #c7c7c7;
$sidebar-box-shadow: -3px 2px 6px -2px $sidebar-box-shadow-color inset, -1px 0 0 0 $sidebar-box-shadow-color inset;
$sidebar-background-color: $table-bg-accent;
$sidebar-active-color: #d93c27;
$sidebar-active-indicator-width: $caret-width-base;
$sidebar-active-box-shadow: -3px 3px 6px -3px;

View File

@ -0,0 +1,84 @@
// Sidebar specific helper variables need to fallback to Bootstrap
// variables. This allows this specific _sidenav user experience
// to easily cascade to a different Bootstrap theme
$sidebar-active-color: $brand-primary !default;
$sidebar-box-shadow: -3px 2px 6px -2px $gray-light inset, -1px 0 0 0 $gray-light inset !default;
#sidebar {
background-color: $sidebar-background-color;
@include box-shadow($sidebar-box-shadow);
height: 100%;
// Legacy padding at the top of nav ... we should remove this.
padding-top: $line-height-computed;
.nav-header > a > .nav-header-title {
text-align: left;
text-transform: none;
padding: $padding-small-vertical 0;
border-bottom: $padding-base-vertical/2 solid $gray-light;
}
.panel {
background-color: transparent;
}
.openstack-dashboard {
& > a {
border-radius: 0;
}
}
.openstack-panel {
& > a {
color: $gray;
padding: $padding-large-vertical $padding-large-horizontal;
}
&.active > a {
font-weight: normal;
color: $sidebar-active-color;
background: $body-bg;
border-bottom: 1px solid $dropdown-divider-bg;
border-left: $sidebar-active-indicator-width solid $sidebar-active-color;
border-top: 1px solid $dropdown-divider-bg;
@include box-shadow($sidebar-active-box-shadow $sidebar-box-shadow-color);
}
}
.openstack-panel > a,
.openstack-dashboard > a.collapsed,
.nav-header > a {
background-color: transparent;
}
.openstack-dashboard > a:hover,
.openstack-dashboard > a:focus,
li > a {
background-color: $nav-pills-active-link-hover-bg;
}
li > a {
@include transition(all 0.3s ease 0s);
outline: 0;
}
// Sets the arrow toggles for each dashboard list
.openstack-dashboard [data-toggle="collapse"] {
.openstack-toggle.fa {
font-size: $font-size-small;
&::before {
content: $fa-var-angle-up;
}
}
&.collapsed {
.openstack-toggle.fa {
@include rotate(180deg);
}
}
}
}