Plugin-based panel configuration
This is an extension to the plugin-based dashboard configuration. It adds support processing panel configuration configuration file in the openstack_dashboard/enabled/ directory. Panels can be added, removed to/from the panel group of a dashboard. It also provide the ability to update the default panel of the dashboard. Change-Id: I2d7adfb8045c244ec063a6741e3b9fe21c188525 Implements: blueprint plugin-panel-config
This commit is contained in:
parent
5be0a3e950
commit
f169ee58ab
@ -434,7 +434,7 @@ settings.
|
||||
|
||||
The default location for the dashboard configuration files is
|
||||
``openstack_dashboard/enabled``, with another directory,
|
||||
``openstack_dashboarrd/local/enabled`` for local overrides. Both sets of files
|
||||
``openstack_dashboard/local/enabled`` for local overrides. Both sets of files
|
||||
will be loaded, but the settings in ``openstack_dashboard/local/enabled`` will
|
||||
overwrite the default ones. The settings are applied in alphabetical order of
|
||||
the filenames. If the same dashboard has configuration files in ``enabled`` and
|
||||
@ -493,3 +493,91 @@ create a file ``openstack_dashboard/local/enabled/_50_tuskar.py`` with::
|
||||
'not_found': exceptions.NOT_FOUND,
|
||||
'unauthorized': exceptions.UNAUTHORIZED,
|
||||
}
|
||||
|
||||
Pluggable Settings for Panels
|
||||
=================================
|
||||
|
||||
Panels customization can be made by providing a custom python module that
|
||||
contains python code to add or remove panel to/from the dashboard. This
|
||||
requires altering the settings file. For panels provided by third-party,
|
||||
making this changes to add the panel is challenging. Panel configuration
|
||||
files can now be dropped to a specified location and it will be read at startup
|
||||
to alter the dashboard configuration.
|
||||
|
||||
The default location for the panel configuration files is
|
||||
``openstack_dashboard/enabled``, with another directory,
|
||||
``openstack_dashboard/local/enabled`` for local overrides. Both sets of files
|
||||
will be loaded, but the settings in ``openstack_dashboard/local/enabled`` will
|
||||
overwrite the default ones. The settings are applied in alphabetical order of
|
||||
the filenames. If the same panel has configuration files in ``enabled`` and
|
||||
``local/enabled``, the local name will be used. Note, that since names of
|
||||
python modules can't start with a digit, the files are usually named with a
|
||||
leading underscore and a number, so that you can control their order easily.
|
||||
|
||||
The files contain following keys:
|
||||
|
||||
``PANEL``
|
||||
-------------
|
||||
|
||||
The name of the panel to be added to ``HORIZON_CONFIG``. Required.
|
||||
|
||||
``PANEL_DASHBOARD``
|
||||
-------------
|
||||
|
||||
The name of the dashboard the ``PANEL`` associated with. Required.
|
||||
|
||||
|
||||
``PANEL_GROUP``
|
||||
-------------
|
||||
|
||||
The name of the panel group the ``PANEL`` is associated with.
|
||||
|
||||
``DEFAULT_PANEL``
|
||||
-----------
|
||||
|
||||
If set, it will update the default panel of the ``PANEL_DASHBOARD``.
|
||||
|
||||
``ADD_PANEL``
|
||||
----------------------
|
||||
|
||||
Python panel class of the ``PANEL`` to be added.
|
||||
|
||||
``REMOVE_PANEL``
|
||||
------------
|
||||
|
||||
If set to ``True``, the PANEL will be removed from PANEL_DASHBOARD/PANEL_GROUP.
|
||||
|
||||
``DISABLED``
|
||||
------------
|
||||
|
||||
If set to ``True``, this panel configuration will be skipped.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
To add a new panel to the Admin panel group in Admin dashboard, create a file
|
||||
``openstack_dashboard/local/enabled/_60_admin_add_panel.py`` with the follwing
|
||||
content::
|
||||
|
||||
PANEL = 'plugin_panel'
|
||||
PANEL_DASHBOARD = 'admin'
|
||||
PANEL_GROUP = 'admin'
|
||||
ADD_PANEL = 'test_panels.plugin_panel.panel.PluginPanel'
|
||||
|
||||
To remove Info panel from Admin panel group in Admin dashboard locally, create
|
||||
a file ``openstack_dashboard/local/enabled/_70_admin_remove_panel.py`` with
|
||||
the following content::
|
||||
|
||||
PANEL = 'info'
|
||||
PANEL_DASHBOARD = 'admin'
|
||||
PANEL_GROUP = 'admin'
|
||||
REMOVE_PANEL = True
|
||||
|
||||
To change the default panel of Admin dashboard to Defaults panel, create a file
|
||||
``openstack_dashboard/local/enabled/_80_admin_default_panel.py`` with the
|
||||
following content::
|
||||
|
||||
PANEL = 'defaults'
|
||||
PANEL_DASHBOARD = 'admin'
|
||||
PANEL_GROUP = 'admin'
|
||||
DEFAULT_PANEL = 'defaults'
|
||||
|
@ -740,6 +740,9 @@ class Site(Registry, HorizonComponent):
|
||||
for dash in self._registry.values():
|
||||
dash._autodiscover()
|
||||
|
||||
# Load the plugin-based panel configuration
|
||||
self._load_panel_customization()
|
||||
|
||||
# Allow for override modules
|
||||
if self._conf.get("customization_module", None):
|
||||
customization_module = self._conf["customization_module"]
|
||||
@ -785,6 +788,61 @@ class Site(Registry, HorizonComponent):
|
||||
if module_has_submodule(mod, mod_name):
|
||||
raise
|
||||
|
||||
def _load_panel_customization(self):
|
||||
"""Applies the plugin-based panel configurations.
|
||||
|
||||
This method parses the panel customization from the ``HORIZON_CONFIG``
|
||||
and make changes to the dashboard accordingly.
|
||||
|
||||
It supports adding, removing and setting default panels on the
|
||||
dashboard.
|
||||
"""
|
||||
panel_customization = self._conf.get("panel_customization", [])
|
||||
|
||||
for config in panel_customization:
|
||||
dashboard = config.get('PANEL_DASHBOARD')
|
||||
if not dashboard:
|
||||
LOG.warning("Skipping %s because it doesn't have "
|
||||
"PANEL_DASHBOARD defined.", config.__name__)
|
||||
continue
|
||||
try:
|
||||
panel_slug = config.get('PANEL')
|
||||
dashboard_cls = self.get_dashboard(dashboard)
|
||||
panel_group = config.get('PANEL_GROUP')
|
||||
default_panel = config.get('DEFAULT_PANEL')
|
||||
|
||||
# Set the default panel
|
||||
if default_panel:
|
||||
dashboard_cls.default_panel = default_panel
|
||||
|
||||
# Remove the panel
|
||||
if config.get('REMOVE_PANEL', False):
|
||||
for panel in dashboard_cls.get_panels():
|
||||
if panel_slug == panel.slug:
|
||||
dashboard_cls.unregister(panel.__class__)
|
||||
elif config.get('ADD_PANEL', None):
|
||||
# Add the panel to the dashboard
|
||||
panel_path = config['ADD_PANEL']
|
||||
mod_path, panel_cls = panel_path.rsplit(".", 1)
|
||||
try:
|
||||
mod = import_module(mod_path)
|
||||
except ImportError:
|
||||
LOG.warning("Could not load panel: %s", mod_path)
|
||||
continue
|
||||
|
||||
panel = getattr(mod, panel_cls)
|
||||
dashboard_cls.register(panel)
|
||||
if panel_group:
|
||||
dashboard_cls.get_panel_group(panel_group).\
|
||||
panels.append(panel.slug)
|
||||
else:
|
||||
panels = list(dashboard_cls.panels)
|
||||
panels.append(panel)
|
||||
dashboard_cls.panels = tuple(panels)
|
||||
except Exception as e:
|
||||
LOG.warning('Could not process panel %(panel)s: %(exc)s',
|
||||
{'panel': panel_slug, 'exc': e})
|
||||
|
||||
|
||||
class HorizonSite(Site):
|
||||
"""A singleton implementation of Site such that all dealings with horizon
|
||||
|
10
openstack_dashboard/enabled/_50_admin_add_panel.py.example
Normal file
10
openstack_dashboard/enabled/_50_admin_add_panel.py.example
Normal file
@ -0,0 +1,10 @@
|
||||
# The name of the panel to be added to HORIZON_CONFIG. Required.
|
||||
PANEL = 'plugin_panel'
|
||||
# The name of the dashboard the PANEL associated with. Required.
|
||||
PANEL_DASHBOARD = 'admin'
|
||||
# The name of the panel group the PANEL is associated with.
|
||||
PANEL_GROUP = 'admin'
|
||||
|
||||
# Python panel class of the PANEL to be added.
|
||||
ADD_PANEL = \
|
||||
'openstack_dashboard.test.test_panels.plugin_panel.panel.PluginPanel'
|
@ -0,0 +1,9 @@
|
||||
# The name of the panel to be added to HORIZON_CONFIG. Required.
|
||||
PANEL = 'info'
|
||||
# The name of the dashboard the PANEL associated with. Required.
|
||||
PANEL_DASHBOARD = 'admin'
|
||||
# The name of the panel group the PANEL is associated with.
|
||||
PANEL_GROUP = 'admin'
|
||||
|
||||
# If set to True, the panel will be removed from PANEL_DASHBOARD/PANEL_GROUP.
|
||||
REMOVE_PANEL = True
|
@ -0,0 +1,9 @@
|
||||
# The name of the panel to be added to HORIZON_CONFIG. Required.
|
||||
PANEL = 'defaults'
|
||||
# The name of the dashboard the PANEL associated with. Required.
|
||||
PANEL_DASHBOARD = 'admin'
|
||||
# The name of the panel group the PANEL is associated with.
|
||||
PANEL_GROUP = 'admin'
|
||||
|
||||
# If set, it will update the default panel of the PANEL_DASHBOARD.
|
||||
DEFAULT_PANEL = 'defaults'
|
0
openstack_dashboard/test/test_panels/__init__.py
Normal file
0
openstack_dashboard/test/test_panels/__init__.py
Normal file
22
openstack_dashboard/test/test_panels/plugin_panel/panel.py
Normal file
22
openstack_dashboard/test/test_panels/plugin_panel/panel.py
Normal file
@ -0,0 +1,22 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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.utils.translation import ugettext_lazy as _
|
||||
|
||||
import horizon
|
||||
|
||||
|
||||
class PluginPanel(horizon.Panel):
|
||||
name = _("Plugin Panel")
|
||||
slug = 'plugin_panel'
|
@ -0,0 +1,15 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Plugin-based Panel" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Plugin-based Panel")%}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
<div class="row-fluid">
|
||||
<div class="span12">
|
||||
Plugin-based Panel
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
22
openstack_dashboard/test/test_panels/plugin_panel/urls.py
Normal file
22
openstack_dashboard/test/test_panels/plugin_panel/urls.py
Normal file
@ -0,0 +1,22 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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 patterns # noqa
|
||||
from django.conf.urls import url # noqa
|
||||
|
||||
from openstack_dashboard.test.test_panels.plugin_panel import views
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
)
|
19
openstack_dashboard/test/test_panels/plugin_panel/views.py
Normal file
19
openstack_dashboard/test/test_panels/plugin_panel/views.py
Normal file
@ -0,0 +1,19 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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.views.generic import TemplateView # noqa
|
||||
|
||||
|
||||
class IndexView(TemplateView):
|
||||
template_name = 'admin/plugin_panel/index.html'
|
0
openstack_dashboard/test/test_plugins/__init__.py
Normal file
0
openstack_dashboard/test/test_plugins/__init__.py
Normal file
@ -0,0 +1,10 @@
|
||||
# The name of the panel to be added to HORIZON_CONFIG. Required.
|
||||
PANEL = 'plugin_panel'
|
||||
# The name of the dashboard the PANEL associated with. Required.
|
||||
PANEL_DASHBOARD = 'admin'
|
||||
# The name of the panel group the PANEL is associated with.
|
||||
PANEL_GROUP = 'admin'
|
||||
|
||||
# Python panel class of the PANEL to be added.
|
||||
ADD_PANEL = \
|
||||
'openstack_dashboard.test.test_panels.plugin_panel.panel.PluginPanel'
|
@ -0,0 +1,9 @@
|
||||
# The name of the panel to be added to HORIZON_CONFIG. Required.
|
||||
PANEL = 'info'
|
||||
# The name of the dashboard the PANEL associated with. Required.
|
||||
PANEL_DASHBOARD = 'admin'
|
||||
# The name of the panel group the PANEL is associated with.
|
||||
PANEL_GROUP = 'admin'
|
||||
|
||||
# If set to True, the panel will be removed from PANEL_DASHBOARD/PANEL_GROUP.
|
||||
REMOVE_PANEL = True
|
@ -0,0 +1,9 @@
|
||||
# The name of the panel to be added to HORIZON_CONFIG. Required.
|
||||
PANEL = 'defaults'
|
||||
# The name of the dashboard the PANEL associated with. Required.
|
||||
PANEL_DASHBOARD = 'admin'
|
||||
# The name of the panel group the PANEL is associated with.
|
||||
PANEL_GROUP = 'admin'
|
||||
|
||||
# If set, it will update the default panel of the PANEL_DASHBOARD.
|
||||
DEFAULT_PANEL = 'defaults'
|
86
openstack_dashboard/test/test_plugins/panel_tests.py
Normal file
86
openstack_dashboard/test/test_plugins/panel_tests.py
Normal file
@ -0,0 +1,86 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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.
|
||||
|
||||
import copy
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import urlresolvers
|
||||
from django.test.utils import override_settings
|
||||
from django.utils.importlib import import_module # noqa
|
||||
|
||||
import horizon
|
||||
from horizon import base
|
||||
from horizon import conf
|
||||
|
||||
from openstack_dashboard.dashboards.admin.info import panel as info_panel
|
||||
from openstack_dashboard.test import helpers as test
|
||||
from openstack_dashboard.test.test_panels.plugin_panel \
|
||||
import panel as plugin_panel
|
||||
import openstack_dashboard.test.test_plugins.panel_config
|
||||
from openstack_dashboard.utils import settings as util_settings
|
||||
|
||||
|
||||
HORIZON_CONFIG = copy.copy(settings.HORIZON_CONFIG)
|
||||
INSTALLED_APPS = list(settings.INSTALLED_APPS)
|
||||
|
||||
util_settings.update_dashboards([
|
||||
openstack_dashboard.test.test_plugins.panel_config,
|
||||
], HORIZON_CONFIG, INSTALLED_APPS)
|
||||
|
||||
|
||||
@override_settings(HORIZON_CONFIG=HORIZON_CONFIG,
|
||||
INSTALLED_APPS=INSTALLED_APPS)
|
||||
class PanelPluginTests(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(PanelPluginTests, self).setUp()
|
||||
self.old_horizon_config = conf.HORIZON_CONFIG
|
||||
conf.HORIZON_CONFIG = conf.LazySettings()
|
||||
base.Horizon._urls()
|
||||
# Trigger discovery, registration, and URLconf generation if it
|
||||
# hasn't happened yet.
|
||||
self.client.get(settings.LOGIN_URL)
|
||||
|
||||
def tearDown(self):
|
||||
super(PanelPluginTests, self).tearDown()
|
||||
conf.HORIZON_CONFIG = self.old_horizon_config
|
||||
# Destroy our singleton and re-create it.
|
||||
base.HorizonSite._instance = None
|
||||
del base.Horizon
|
||||
base.Horizon = base.HorizonSite()
|
||||
self._reload_urls()
|
||||
|
||||
def _reload_urls(self):
|
||||
"""Clears out the URL caches, reloads the root urls module, and
|
||||
re-triggers the autodiscovery mechanism for Horizon. Allows URLs
|
||||
to be re-calculated after registering new dashboards. Useful
|
||||
only for testing and should never be used on a live site.
|
||||
"""
|
||||
urlresolvers.clear_url_caches()
|
||||
reload(import_module(settings.ROOT_URLCONF))
|
||||
base.Horizon._urls()
|
||||
|
||||
def test_add_panel(self):
|
||||
dashboard = horizon.get_dashboard("admin")
|
||||
self.assertIn(plugin_panel.PluginPanel,
|
||||
[p.__class__ for p in dashboard.get_panels()])
|
||||
|
||||
def test_remove_panel(self):
|
||||
dashboard = horizon.get_dashboard("admin")
|
||||
self.assertNotIn(info_panel.Info,
|
||||
[p.__class__ for p in dashboard.get_panels()])
|
||||
|
||||
def test_default_panel(self):
|
||||
dashboard = horizon.get_dashboard("admin")
|
||||
self.assertEqual(dashboard.default_panel, 'defaults')
|
@ -41,19 +41,21 @@ def import_dashboard_config(modules):
|
||||
config = collections.defaultdict(dict)
|
||||
for module in modules:
|
||||
for key, submodule in import_submodules(module).iteritems():
|
||||
try:
|
||||
if hasattr(submodule, 'DASHBOARD'):
|
||||
dashboard = submodule.DASHBOARD
|
||||
except AttributeError:
|
||||
logging.warning("Skipping %s because it doesn't "
|
||||
"have DASHBOARD defined." % submodule.__name__)
|
||||
else:
|
||||
config[dashboard].update(submodule.__dict__)
|
||||
elif hasattr(submodule, 'PANEL'):
|
||||
config[submodule.__name__] = submodule.__dict__
|
||||
#_update_panels(config, submodule)
|
||||
else:
|
||||
logging.warning("Skipping %s because it doesn't have DASHBOARD"
|
||||
" or PANEL defined.", submodule.__name__)
|
||||
return sorted(config.iteritems(),
|
||||
key=lambda c: c[1]['__name__'].rsplit('.', 1))
|
||||
|
||||
|
||||
def update_dashboards(modules, horizon_config, installed_apps):
|
||||
"""Imports dashboard configuration from modules and applies it.
|
||||
"""Imports dashboard and panel configuration from modules and applies it.
|
||||
|
||||
The submodules from specified modules are imported, and the configuration
|
||||
for the specific dashboards is merged, with the later modules overriding
|
||||
@ -75,18 +77,30 @@ def update_dashboards(modules, horizon_config, installed_apps):
|
||||
configurations will be applied in order ``qux``, ``baz`` (``baz`` is
|
||||
second, because the most recent file which contributed to it, ``_30_baz``,
|
||||
comes after ``_20_qux``).
|
||||
|
||||
Panel specific configurations are stored in horizon_config. Dashboards
|
||||
from both plugin-based and openstack_dashboard must be registered before
|
||||
the panel configuration can be applied. Making changes to the panel is
|
||||
deferred until the horizon autodiscover is completed, configurations are
|
||||
applied in alphabetical order of files where it was imported.
|
||||
"""
|
||||
dashboards = []
|
||||
exceptions = {}
|
||||
apps = []
|
||||
for dashboard, config in import_dashboard_config(modules):
|
||||
panel_customization = []
|
||||
for key, config in import_dashboard_config(modules):
|
||||
if config.get('DISABLED', False):
|
||||
continue
|
||||
dashboards.append(dashboard)
|
||||
exceptions.update(config.get('ADD_EXCEPTIONS', {}))
|
||||
apps.extend(config.get('ADD_INSTALLED_APPS', []))
|
||||
if config.get('DEFAULT', False):
|
||||
horizon_config['default_dashboard'] = dashboard
|
||||
if config.get('DASHBOARD'):
|
||||
dashboard = key
|
||||
dashboards.append(dashboard)
|
||||
exceptions.update(config.get('ADD_EXCEPTIONS', {}))
|
||||
apps.extend(config.get('ADD_INSTALLED_APPS', []))
|
||||
if config.get('DEFAULT', False):
|
||||
horizon_config['default_dashboard'] = dashboard
|
||||
elif config.get('PANEL'):
|
||||
panel_customization.append(config)
|
||||
horizon_config['panel_customization'] = panel_customization
|
||||
horizon_config['dashboards'] = tuple(dashboards)
|
||||
horizon_config['exceptions'].update(exceptions)
|
||||
installed_apps.extend(apps)
|
||||
|
Loading…
x
Reference in New Issue
Block a user