Merge "Adds extensible header functionality"

This commit is contained in:
Jenkins 2017-10-13 16:25:35 +00:00 committed by Gerrit Code Review
commit d4b93486bd
16 changed files with 200 additions and 1 deletions

3
doc/source/contributor/tutorials/plugin.rst Normal file → Executable file
View File

@ -149,6 +149,9 @@ _31000_myplugin.py::
# A list of scss files to be included in the compressed set of files
ADD_SCSS_FILES = ['dashboard/identity/myplugin/mypanel/mypanel.scss']
# A list of template-based views to be added to the header
ADD_HEADER_SECTIONS = ['myplugin.content.mypanel.views.HeaderView',]
.. Note ::
Currently, AUTO_DISCOVER_STATIC_FILES = True will only discover JavaScript files,

View File

@ -0,0 +1,65 @@
/**
* 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
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
/* Core functionality related to extensible header sections. */
horizon.extensible_header = {
populate: function() {
var $path = $(location).attr('pathname');
var $url = $(location).attr('href');
$url = $url.replace($path, $(window).attr('WEBROOT') + 'header/');
horizon.ajax.queue({
url: $url,
success: function(data) {
$('#extensible-header').replaceWith($(data));
selected = horizon.cookies.get('selected_header');
if(selected && $('#header-list #' + selected).length){
$old_primary = $('#primary-extensible-header > a');
$new_primary = $('#header-list #' + selected);
$old_primary.insertAfter($new_primary);
$new_primary.first().appendTo($('#primary-extensible-header'));
}
function swap() {
$old_primary = $('#primary-extensible-header > a');
$new_primary = $(this);
horizon.cookies.put("selected_header", $new_primary.attr('id'), {path:'/'});
$old_primary.insertAfter($new_primary);
$new_primary.appendTo($('#primary-extensible-header'));
$new_primary.off('click', swap);
$old_primary.on('click', swap);
}
$('#header-list .extensible-header-section').on('click', swap);
},
error: function(jqXHR) {
if (jqXHR.status !== 401 && jqXHR.status !== 403) {
// error is raised with status of 0 when ajax query is cancelled
// due to new page request
if (jqXHR.status !== 0) {
horizon.alert("info", gettext("Failed to populate extensible header."));
}
}
}
});
return true;
}
};
horizon.addInitFunction(function() {
// trigger extensible header section query on page load
horizon.extensible_header.populate();
});

View File

@ -23,6 +23,11 @@
margin-bottom: 1px;
}
.navbar-nav .header-overflow ul li{
white-space: nowrap;
padding: $bs-dropdown-item-padding-vertical $bs-dropdown-item-padding-horizontal;
}
.dropdown-toggle > .fa {
padding-left: $padding-small-vertical;
padding-right: $padding-small-vertical;

1
openstack_dashboard/templates/header/_header.html Normal file → Executable file
View File

@ -29,6 +29,7 @@
</ul>
<ul class="nav navbar-nav navbar-right">
<li id="extensible-header" class="extensible-header"></li>
{% if profiler_enabled %}
{% include "developer/profiler/_mode_picker.html" %}
{% endif %}

View File

@ -0,0 +1,28 @@
{% load i18n %}
<li id="primary-extensible-header" class="dropdown extensible-header">
{% for header in header_sections%}
{% if forloop.first %}
<a id="{{ header.0 }}" class="extensible-header-section"> {% autoescape off %}{{ header.1 }}{% endautoescape %} </a>
{% endif %}
{% endfor %}
</li>
{% if header_sections|length > 1 %}
<li class="dropdown header-overflow">
<a data-toggle="dropdown" href="#" class="dropdown-toggle" role="button" aria-expanded="false">
<span>{% trans "More" %}</span>
<span class="fa fa-caret-down"></span>
</a>
<ul id="header-list" class="dropdown-menu dropdown-menu-right selection-menu">
{% for header in header_sections%}
{% if not forloop.first %}
<li>
<a id="{{ header.0 }}" class="extensible-header-section"> {% autoescape off %}{{ header.1 }}{% endautoescape %} </a>
</li>
{% endif %}
{% endfor %}
</ul>
</li>
{% endif %}

1
openstack_dashboard/templates/horizon/_scripts.html Normal file → Executable file
View File

@ -44,6 +44,7 @@
<script src='{{ STATIC_URL }}horizon/js/horizon.d3linechart.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.d3barchart.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.sidebar.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.extensible_header.js'></script>
<script src='{{ STATIC_URL }}js/horizon.instances.js'></script>
<script src='{{ STATIC_URL }}js/horizon.quota.js'></script>
<script src='{{ STATIC_URL }}js/horizon.metering.js'></script>

View File

@ -0,0 +1,18 @@
# 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
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf.urls import url
from openstack_dashboard.urls import urlpatterns # noqa
from openstack_dashboard import views
urlpatterns.append(url(r'^header/', views.ExtensibleHeaderView.as_view()))

View File

@ -0,0 +1,6 @@
{% load i18n %}
<span class="test-header">
<strong>{% trans "This is a test: " %} </strong>
<span class="badge">{{ message }}</span>
</span>

View File

@ -16,3 +16,13 @@ from horizon import views
class IndexView(views.HorizonTemplateView):
template_name = 'admin/plugin_panel/index.html'
page_title = 'Plugin-based Panel'
class TestBannerView(views.HorizonTemplateView):
template_name = 'admin/plugin_panel/header.html'
def get_context_data(self, **kwargs):
context = super(TestBannerView, self).get_context_data(**kwargs)
context['message'] = "sample context"
return context

View File

@ -23,3 +23,7 @@ ADD_JS_SPEC_FILES = ['plugin_panel/plugin.spec.js']
# A list of scss files to be included in the compressed set of files
ADD_SCSS_FILES = ['plugin_panel/plugin.scss']
# A list of extensible header views to be displayed
ADD_HEADER_SECTIONS = \
['openstack_dashboard.test.test_panels.plugin_panel.views.TestBannerView',]

8
openstack_dashboard/test/test_plugins/panel_tests.py Normal file → Executable file
View File

@ -44,6 +44,8 @@ util_settings.update_dashboards([panel_config,], HORIZON_CONFIG, INSTALLED_APPS)
@override_settings(HORIZON_CONFIG=HORIZON_CONFIG,
INSTALLED_APPS=INSTALLED_APPS)
class PanelPluginTests(test.PluginTestCase):
urls = 'openstack_dashboard.test.extensible_header_urls'
def test_add_panel(self):
dashboard = horizon.get_dashboard("admin")
panel_group = dashboard.get_panel_group('admin')
@ -58,6 +60,12 @@ class PanelPluginTests(test.PluginTestCase):
self.assertEqual(pc.ADD_JS_FILES, HORIZON_CONFIG['js_files'])
self.assertEqual(pc.ADD_JS_SPEC_FILES, HORIZON_CONFIG['js_spec_files'])
self.assertEqual(pc.ADD_SCSS_FILES, HORIZON_CONFIG['scss_files'])
self.assertEqual(pc.ADD_HEADER_SECTIONS,
HORIZON_CONFIG['header_sections'])
def test_extensible_header(self):
response = self.client.get('/header/')
self.assertIn('sample context', response.content)
def test_remove_panel(self):
dashboard = horizon.get_dashboard("admin")

View File

@ -29,6 +29,7 @@
</ul>
<ul class="nav navbar-nav navbar-right">
<li id="extensible-header" class="extensible-header"></li>
{% if profiler_enabled %}
{% include "developer/profiler/_mode_picker.html" %}
{% endif %}

1
openstack_dashboard/urls.py Normal file → Executable file
View File

@ -35,6 +35,7 @@ from openstack_dashboard import views
urlpatterns = [
url(r'^$', views.splash, name='splash'),
url(r'^api/', include(rest.urls)),
url(r'^header/', views.ExtensibleHeaderView.as_view()),
url(r'', include(horizon.urls)),
]

5
openstack_dashboard/utils/settings.py Normal file → Executable file
View File

@ -110,6 +110,7 @@ def update_dashboards(modules, horizon_config, installed_apps):
js_spec_files = []
scss_files = []
panel_customization = []
header_sections = []
update_horizon_config = {}
for key, config in import_dashboard_config(modules):
if config.get('DISABLED', False):
@ -120,6 +121,9 @@ def update_dashboards(modules, horizon_config, installed_apps):
_apps = config.get('ADD_INSTALLED_APPS', [])
apps.extend(_apps)
_header_sections = config.get('ADD_HEADER_SECTIONS', [])
header_sections.extend(_header_sections)
if config.get('AUTO_DISCOVER_STATIC_FILES', False):
for _app in _apps:
module = import_module(_app)
@ -156,6 +160,7 @@ def update_dashboards(modules, horizon_config, installed_apps):
if d not in config_dashboards])
horizon_config['panel_customization'] = panel_customization
horizon_config['header_sections'] = header_sections
horizon_config['dashboards'] = tuple(dashboards)
horizon_config.setdefault('exceptions', {}).update(exceptions)
horizon_config.update(update_horizon_config)

38
openstack_dashboard/views.py Normal file → Executable file
View File

@ -12,10 +12,14 @@
# License for the specific language governing permissions and limitations
# under the License.
from importlib import import_module
import logging
from django.conf import settings
from django.core import urlresolvers
from django import shortcuts
import django.views.decorators.vary
from django.views.generic import TemplateView
from six.moves import urllib
import horizon
@ -23,6 +27,8 @@ from horizon import base
from horizon import exceptions
from horizon import notifications
LOG = logging.getLogger(__name__)
MESSAGES_PATH = getattr(settings, 'MESSAGES_PATH', None)
@ -80,3 +86,35 @@ def get_url_with_pagination(request, marker_name, prev_marker_name, url_string,
urllib.parse.urlencode({prev_marker_name:
prev_marker}))
return url
class ExtensibleHeaderView(TemplateView):
template_name = 'header/_header_sections.html'
def get_context_data(self, **kwargs):
context = super(ExtensibleHeaderView, self).get_context_data(**kwargs)
header_sections = []
config = getattr(settings, 'HORIZON_CONFIG', {})
for view_path in config.get("header_sections", []):
mod_path, view_cls = view_path.rsplit(".", 1)
try:
mod = import_module(mod_path)
except ImportError:
LOG.warning("Could not load header view: %s", mod_path)
continue
try:
view = getattr(mod, view_cls)(request=self.request)
response = view.get(self.request)
rendered_response = response.render()
packed_response = [view_path.replace('.', '-'),
rendered_response.content]
header_sections.append(packed_response)
except Exception as e:
LOG.warning("Could not render header %(path)s, exception: "
"%(exc)s", {'path': view_path, 'exc': e})
continue
context['header_sections'] = header_sections
return context

View File

@ -0,0 +1,5 @@
---
features:
- >
[`blueprint extensible-header <https://blueprints.launchpad.net/horizon/+spec/extensible-header>`_]
Added a feature to insert custom headers into horizon's topbar.