Merge "Adds extensible header functionality"
This commit is contained in:
commit
d4b93486bd
3
doc/source/contributor/tutorials/plugin.rst
Normal file → Executable file
3
doc/source/contributor/tutorials/plugin.rst
Normal file → Executable 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,
|
||||
|
65
horizon/static/horizon/js/horizon.extensible_header.js
Executable file
65
horizon/static/horizon/js/horizon.extensible_header.js
Executable 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();
|
||||
});
|
5
openstack_dashboard/static/dashboard/scss/components/_navbar.scss
Normal file → Executable file
5
openstack_dashboard/static/dashboard/scss/components/_navbar.scss
Normal file → Executable 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
1
openstack_dashboard/templates/header/_header.html
Normal file → Executable 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 %}
|
||||
|
28
openstack_dashboard/templates/header/_header_sections.html
Executable file
28
openstack_dashboard/templates/header/_header_sections.html
Executable 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
1
openstack_dashboard/templates/horizon/_scripts.html
Normal file → Executable 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>
|
||||
|
18
openstack_dashboard/test/extensible_header_urls.py
Executable file
18
openstack_dashboard/test/extensible_header_urls.py
Executable 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()))
|
@ -0,0 +1,6 @@
|
||||
{% load i18n %}
|
||||
|
||||
<span class="test-header">
|
||||
<strong>{% trans "This is a test: " %} </strong>
|
||||
<span class="badge">{{ message }}</span>
|
||||
</span>
|
10
openstack_dashboard/test/test_panels/plugin_panel/views.py
Normal file → Executable file
10
openstack_dashboard/test/test_panels/plugin_panel/views.py
Normal file → Executable 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
|
||||
|
6
openstack_dashboard/test/test_plugins/panel_config/_10_admin_add_panel.py
Normal file → Executable file
6
openstack_dashboard/test/test_plugins/panel_config/_10_admin_add_panel.py
Normal file → Executable file
@ -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']
|
||||
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
8
openstack_dashboard/test/test_plugins/panel_tests.py
Normal file → Executable 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")
|
||||
|
1
openstack_dashboard/themes/material/templates/header/_header.html
Normal file → Executable file
1
openstack_dashboard/themes/material/templates/header/_header.html
Normal file → Executable 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
1
openstack_dashboard/urls.py
Normal file → Executable 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
5
openstack_dashboard/utils/settings.py
Normal file → Executable 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
38
openstack_dashboard/views.py
Normal file → Executable 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
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user