diff --git a/doc/source/contributor/tutorials/plugin.rst b/doc/source/contributor/tutorials/plugin.rst
old mode 100644
new mode 100755
index dd77b4e303..1e740e1917
--- a/doc/source/contributor/tutorials/plugin.rst
+++ b/doc/source/contributor/tutorials/plugin.rst
@@ -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,
diff --git a/horizon/static/horizon/js/horizon.extensible_header.js b/horizon/static/horizon/js/horizon.extensible_header.js
new file mode 100755
index 0000000000..bdbec77210
--- /dev/null
+++ b/horizon/static/horizon/js/horizon.extensible_header.js
@@ -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();
+});
diff --git a/openstack_dashboard/static/dashboard/scss/components/_navbar.scss b/openstack_dashboard/static/dashboard/scss/components/_navbar.scss
old mode 100644
new mode 100755
index 6a6ecf7c12..a8825e4d5a
--- a/openstack_dashboard/static/dashboard/scss/components/_navbar.scss
+++ b/openstack_dashboard/static/dashboard/scss/components/_navbar.scss
@@ -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;
diff --git a/openstack_dashboard/templates/header/_header.html b/openstack_dashboard/templates/header/_header.html
old mode 100644
new mode 100755
index c1aa83357b..81bb54dd46
--- a/openstack_dashboard/templates/header/_header.html
+++ b/openstack_dashboard/templates/header/_header.html
@@ -29,6 +29,7 @@
+
{% if profiler_enabled %}
{% include "developer/profiler/_mode_picker.html" %}
{% endif %}
diff --git a/openstack_dashboard/templates/header/_header_sections.html b/openstack_dashboard/templates/header/_header_sections.html
new file mode 100755
index 0000000000..85113bf76f
--- /dev/null
+++ b/openstack_dashboard/templates/header/_header_sections.html
@@ -0,0 +1,28 @@
+{% load i18n %}
+
+
+
+{% if header_sections|length > 1 %}
+
+{% endif %}
diff --git a/openstack_dashboard/templates/horizon/_scripts.html b/openstack_dashboard/templates/horizon/_scripts.html
old mode 100644
new mode 100755
index e528e3e4b1..78b0430ce4
--- a/openstack_dashboard/templates/horizon/_scripts.html
+++ b/openstack_dashboard/templates/horizon/_scripts.html
@@ -44,6 +44,7 @@
+
diff --git a/openstack_dashboard/test/extensible_header_urls.py b/openstack_dashboard/test/extensible_header_urls.py
new file mode 100755
index 0000000000..fb481b00a0
--- /dev/null
+++ b/openstack_dashboard/test/extensible_header_urls.py
@@ -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()))
diff --git a/openstack_dashboard/test/test_panels/plugin_panel/templates/plugin_panel/header.html b/openstack_dashboard/test/test_panels/plugin_panel/templates/plugin_panel/header.html
new file mode 100755
index 0000000000..4f711fcdd1
--- /dev/null
+++ b/openstack_dashboard/test/test_panels/plugin_panel/templates/plugin_panel/header.html
@@ -0,0 +1,6 @@
+{% load i18n %}
+
+
diff --git a/openstack_dashboard/test/test_panels/plugin_panel/views.py b/openstack_dashboard/test/test_panels/plugin_panel/views.py
old mode 100644
new mode 100755
index 49c4f7ad20..a5c18fda40
--- a/openstack_dashboard/test/test_panels/plugin_panel/views.py
+++ b/openstack_dashboard/test/test_panels/plugin_panel/views.py
@@ -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
diff --git a/openstack_dashboard/test/test_plugins/panel_config/_10_admin_add_panel.py b/openstack_dashboard/test/test_plugins/panel_config/_10_admin_add_panel.py
old mode 100644
new mode 100755
index 543e383171..6e0b047b88
--- a/openstack_dashboard/test/test_plugins/panel_config/_10_admin_add_panel.py
+++ b/openstack_dashboard/test/test_plugins/panel_config/_10_admin_add_panel.py
@@ -22,4 +22,8 @@ ADD_JS_FILES = ['plugin_panel/plugin_module.js']
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']
\ No newline at end of file
+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',]
diff --git a/openstack_dashboard/test/test_plugins/panel_tests.py b/openstack_dashboard/test/test_plugins/panel_tests.py
old mode 100644
new mode 100755
index 5cb3787a66..34f06f98c7
--- a/openstack_dashboard/test/test_plugins/panel_tests.py
+++ b/openstack_dashboard/test/test_plugins/panel_tests.py
@@ -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")
diff --git a/openstack_dashboard/themes/material/templates/header/_header.html b/openstack_dashboard/themes/material/templates/header/_header.html
old mode 100644
new mode 100755
index 0d97618bef..07d4bd60dd
--- a/openstack_dashboard/themes/material/templates/header/_header.html
+++ b/openstack_dashboard/themes/material/templates/header/_header.html
@@ -29,6 +29,7 @@
+
{% if profiler_enabled %}
{% include "developer/profiler/_mode_picker.html" %}
{% endif %}
diff --git a/openstack_dashboard/urls.py b/openstack_dashboard/urls.py
old mode 100644
new mode 100755
index 1992239259..eba0cc9425
--- a/openstack_dashboard/urls.py
+++ b/openstack_dashboard/urls.py
@@ -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)),
]
diff --git a/openstack_dashboard/utils/settings.py b/openstack_dashboard/utils/settings.py
old mode 100644
new mode 100755
index 65b26c4295..8a760deeeb
--- a/openstack_dashboard/utils/settings.py
+++ b/openstack_dashboard/utils/settings.py
@@ -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)
diff --git a/openstack_dashboard/views.py b/openstack_dashboard/views.py
old mode 100644
new mode 100755
index 4861134566..f91a30fb84
--- a/openstack_dashboard/views.py
+++ b/openstack_dashboard/views.py
@@ -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
diff --git a/releasenotes/notes/extensible-header-ac3c94f3057c1b2a.yaml b/releasenotes/notes/extensible-header-ac3c94f3057c1b2a.yaml
new file mode 100644
index 0000000000..adfc761e9e
--- /dev/null
+++ b/releasenotes/notes/extensible-header-ac3c94f3057c1b2a.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - >
+ [`blueprint extensible-header `_]
+ Added a feature to insert custom headers into horizon's topbar.