5bd6658672
Changing tutorial to use the newer method for loading dashboards. The tutorial needs a new revision in general, but that is beyond the scope of this change. Closes-bug: #1277325 Change-Id: Ie197b5ad9aafa125235a839fbcc7cd95e352e96a
554 lines
19 KiB
ReStructuredText
554 lines
19 KiB
ReStructuredText
===================
|
|
Building on Horizon
|
|
===================
|
|
|
|
This tutorial covers how to use the various components in Horizon to build
|
|
an example dashboard and panel with a data table and tabs.
|
|
|
|
As an example, we'll build on the Nova instances API to create a new and novel
|
|
"visualizations" dashboard with a "flocking" panel that presents the instance
|
|
data in a different manner.
|
|
|
|
You can find a reference implementation of the code being described here
|
|
on github at https://github.com/gabrielhurley/horizon_demo.
|
|
|
|
.. note::
|
|
|
|
There are a variety of other resources which may be helpful to read first,
|
|
since this is a more advanced tutorial. For example, you may want to start
|
|
with the :doc:`Horizon quickstart guide </quickstart>` or the
|
|
`Django tutorial`_.
|
|
|
|
.. _Django tutorial: https://docs.djangoproject.com/en/1.4/intro/tutorial01/
|
|
|
|
|
|
Creating a dashboard
|
|
====================
|
|
|
|
.. note::
|
|
|
|
It is perfectly valid to create a panel without a dashboard, and
|
|
incorporate it into an existing dashboard. See the section
|
|
:ref:`overrides <overrides>` later in this document.
|
|
|
|
The quick version
|
|
-----------------
|
|
|
|
Horizon provides a custom management command to create a typical base
|
|
dashboard structure for you. The following command generates most of the
|
|
boilerplate code explained below::
|
|
|
|
./run_tests.sh -m startdash visualizations
|
|
|
|
It's still recommended that you read the rest of this section to understand
|
|
what that command creates and why.
|
|
|
|
Structure
|
|
---------
|
|
|
|
The recommended structure for a dashboard (or panel) follows suit with the
|
|
typical Django application layout. We'll name our dashboard "visualizations"::
|
|
|
|
visualizations
|
|
|--__init__.py
|
|
|--dashboard.py
|
|
|--templates/
|
|
|--static/
|
|
|
|
The ``dashboard.py`` module will contain our dashboard class for use by
|
|
Horizon; the ``templates`` and ``static`` directories give us homes for our
|
|
Django template files and static media respectively.
|
|
|
|
Within the ``static`` and ``templates`` directories it's generally good to
|
|
namespace your files like so::
|
|
|
|
templates/
|
|
|--visualizations/
|
|
static/
|
|
|--visualizations/
|
|
|--css/
|
|
|--js/
|
|
|--img/
|
|
|
|
With those files and directories in place, we can move on to writing our
|
|
dashboard class.
|
|
|
|
|
|
Defining a dashboard
|
|
--------------------
|
|
|
|
A dashboard class can be incredibly simple (about 3 lines at minimum),
|
|
defining nothing more than a name and a slug::
|
|
|
|
import horizon
|
|
|
|
class VizDash(horizon.Dashboard):
|
|
name = _("Visualizations")
|
|
slug = "visualizations"
|
|
|
|
In practice, a dashboard class will usually contain more information, such as a
|
|
list of panels, which panel is the default, and any permissions required to
|
|
access this dashboard::
|
|
|
|
class VizDash(horizon.Dashboard):
|
|
name = _("Visualizations")
|
|
slug = "visualizations"
|
|
panels = ('flocking',)
|
|
default_panel = 'flocking'
|
|
permissions = ('openstack.roles.admin',)
|
|
|
|
Building from that previous example we may also want to define a grouping of
|
|
panels which share a common theme and have a sub-heading in the navigation::
|
|
|
|
class InstanceVisualizations(horizon.PanelGroup):
|
|
slug = "instance_visualizations"
|
|
name = _("Instance Visualizations")
|
|
panels = ('flocking',)
|
|
|
|
|
|
class VizDash(horizon.Dashboard):
|
|
name = _("Visualizations")
|
|
slug = "visualizations"
|
|
panels = (InstanceVisualizations,)
|
|
default_panel = 'flocking'
|
|
permissions = ('openstack.roles.admin',)
|
|
|
|
The ``PanelGroup`` can be added to the dashboard class' ``panels`` list
|
|
just like the slug of the panel can.
|
|
|
|
Once our dashboard class is complete, all we need to do is register it::
|
|
|
|
horizon.register(VizDash)
|
|
|
|
The typical place for that would be the bottom of the ``dashboard.py`` file,
|
|
but it could also go elsewhere, such as in an override file (see below).
|
|
|
|
|
|
Creating a panel
|
|
================
|
|
|
|
Now that we have our dashboard written, we can also create our panel. We'll
|
|
call it "flocking".
|
|
|
|
.. note::
|
|
|
|
You don't need to write a custom dashboard to add a panel. The structure
|
|
here is for the sake of completeness in the tutorial.
|
|
|
|
The quick version
|
|
-----------------
|
|
|
|
Horizon provides a custom management command to create a typical base
|
|
panel structure for you. The following command generates most of the
|
|
boilerplate code explained below::
|
|
|
|
./run_tests.sh -m startpanel flocking --dashboard=visualizations --target=auto
|
|
|
|
The ``dashboard`` argument is required, and tells the command which dashboard
|
|
this panel will be registered with. The ``target`` argument is optional, and
|
|
respects ``auto`` as a special value which means that the files for the panel
|
|
should be created inside the dashboard module as opposed to the current
|
|
directory (the default).
|
|
|
|
It's still recommended that you read the rest of this section to understand
|
|
what that command creates and why.
|
|
|
|
Structure
|
|
---------
|
|
|
|
A panel is a relatively flat structure with the exception that templates
|
|
for a panel in a dashboard live in the dashboard's ``templates`` directory
|
|
rather than in the panel's ``templates`` directory. Continuing our
|
|
visualization/flocking example, let's see what the file structure looks like::
|
|
|
|
# stand-alone panel structure
|
|
flocking/
|
|
|--__init__.py
|
|
|--panel.py
|
|
|--urls.py
|
|
|--views.py
|
|
|--templates/
|
|
|--flocking/
|
|
|--index.html
|
|
|
|
# panel-in-a-dashboard structure
|
|
visualizations/
|
|
|--__init__.py
|
|
|--dashboard.py
|
|
|--flocking/
|
|
|--__init__.py
|
|
|--panel.py
|
|
|--urls.py
|
|
|--views.py
|
|
|--templates/
|
|
|--visualizations/
|
|
|--flocking/
|
|
|--index.html
|
|
|
|
That follows standard Django namespacing conventions for apps and submodules
|
|
within apps. It also works cleanly with Django's automatic template discovery
|
|
in both cases.
|
|
|
|
Defining a panel
|
|
----------------
|
|
|
|
The ``panel.py`` file referenced above has a special meaning. Within a
|
|
dashboard, any module name listed in the ``panels`` attribute on the
|
|
dashboard class will be auto-discovered by looking for ``panel.py`` file
|
|
in a corresponding directory (the details are a bit magical, but have been
|
|
thoroughly vetted in Django's admin codebase).
|
|
|
|
Inside the ``panel.py`` module we define our ``Panel`` class::
|
|
|
|
class Flocking(horizon.Panel):
|
|
name = _("Flocking")
|
|
slug = 'flocking'
|
|
|
|
Simple, right? Once we've defined it, we register it with the dashboard::
|
|
|
|
from visualizations import dashboard
|
|
|
|
dashboard.VizDash.register(Flocking)
|
|
|
|
Easy! There are more options you can set to customize the ``Panel`` class, but
|
|
it makes some intelligent guesses about what the defaults should be.
|
|
|
|
URLs
|
|
----
|
|
|
|
One of the intelligent assumptions the ``Panel`` class makes is that it can
|
|
find a ``urls.py`` file in your panel directory which will define a view named
|
|
``index`` that handles the default view for that panel. This is what your
|
|
``urls.py`` file might look like::
|
|
|
|
from django.conf.urls import patterns, url
|
|
from .views import IndexView
|
|
|
|
urlpatterns = patterns('',
|
|
url(r'^$', IndexView.as_view(), name='index')
|
|
)
|
|
|
|
There's nothing there that isn't 100% standard Django code. This example
|
|
(and Horizon in general) uses the class-based views introduced in Django 1.3
|
|
to make code more reusable. Hence the view class is imported in the example
|
|
above, and the ``as_view()`` method is called in the URL pattern.
|
|
|
|
This, of course, presumes you have a view class, and takes us into the meat
|
|
of writing a ``Panel``.
|
|
|
|
|
|
Tables, Tabs, and Views
|
|
-----------------------
|
|
|
|
Now we get to the really exciting parts; everything before this was structural.
|
|
|
|
Starting with the high-level view, our end goal is to create a view (our
|
|
``IndexView`` class referenced above) which uses Horizon's ``DataTable``
|
|
class to display data and Horizon's ``TabGroup`` class to give us a
|
|
user-friendly tabbed interface in the browser.
|
|
|
|
We'll start with the table, combine that with the tabs, and then build our
|
|
view from the pieces.
|
|
|
|
Defining a table
|
|
~~~~~~~~~~~~~~~~
|
|
|
|
Horizon provides a :class:`~horizon.tables.DataTable` class which simplifies
|
|
the vast majority of displaying data to an end-user. We're just going to skim
|
|
the surface here, but it has a tremendous number of capabilities.
|
|
|
|
In this case, we're going to be presenting data about tables, so let's start
|
|
defining our table (and a ``tables.py`` module::
|
|
|
|
from horizon import tables
|
|
|
|
class FlockingInstancesTable(tables.DataTable):
|
|
host = tables.Column("OS-EXT-SRV-ATTR:host", verbose_name=_("Host"))
|
|
tenant = tables.Column('tenant_name', verbose_name=_("Tenant"))
|
|
user = tables.Column('user_name', verbose_name=_("user"))
|
|
vcpus = tables.Column('flavor_vcpus', verbose_name=_("VCPUs"))
|
|
memory = tables.Column('flavor_memory', verbose_name=_("Memory"))
|
|
age = tables.Column('age', verbose_name=_("Age"))
|
|
|
|
class Meta:
|
|
name = "instances"
|
|
verbose_name = _("Instances")
|
|
|
|
There are several things going on here... we created a table subclass,
|
|
and defined six columns on it. Each of those columns defines what attribute
|
|
it accesses on the instance object as the first argument, and since we like to
|
|
make everything translatable, we give each column a ``verbose_name`` that's
|
|
marked for translation.
|
|
|
|
Lastly, we added a ``Meta`` class which defines some properties about our
|
|
table, notably its (translatable) verbose name, and a semi-unique "slug"-like
|
|
name to identify it.
|
|
|
|
.. note::
|
|
|
|
This is a slight simplification from the reality of how the instance
|
|
object is actually structured. In reality, accessing the flavor, tenant,
|
|
and user attributes on it requires an additional step. This code can be
|
|
seen in the example code available on github.
|
|
|
|
Defining tabs
|
|
~~~~~~~~~~~~~
|
|
|
|
So we have a table, ready to receive our data. We could go straight to a view
|
|
from here, but we can think bigger. In this case we're also going to use
|
|
Horizon's :class:`~horizon.tabs.TabGroup` class. This gives us a clean,
|
|
no-fuss tabbed interface to display both our visualization and, optionally,
|
|
our data table.
|
|
|
|
First off, let's make a tab for our visualization::
|
|
|
|
class VizTab(tabs.Tab):
|
|
name = _("Visualization")
|
|
slug = "viz"
|
|
template_name = "visualizations/flocking/_flocking.html"
|
|
|
|
def get_context_data(self, request):
|
|
return None
|
|
|
|
This is about as simple as you can get. Since our visualization will
|
|
ultimately use AJAX to load it's data we don't need to pass any context
|
|
to the template, and all we need to define is the name and which template
|
|
it should use.
|
|
|
|
Now, we also need a tab for our data table::
|
|
|
|
from .tables import FlockingInstancesTable
|
|
|
|
class DataTab(tabs.TableTab):
|
|
name = _("Data")
|
|
slug = "data"
|
|
table_classes = (FlockingInstancesTable,)
|
|
template_name = "horizon/common/_detail_table.html"
|
|
preload = False
|
|
|
|
def get_instances_data(self):
|
|
try:
|
|
instances = utils.get_instances_data(self.tab_group.request)
|
|
except:
|
|
instances = []
|
|
exceptions.handle(self.tab_group.request,
|
|
_('Unable to retrieve instance list.'))
|
|
return instances
|
|
|
|
This tab gets a little more complicated. Foremost, it's a special type of
|
|
tab--one that handles data tables (and all their associated features)--and
|
|
it also uses the ``preload`` attribute to specify that this tab shouldn't
|
|
be loaded by default. It will instead be loaded via AJAX when someone clicks
|
|
on it, saving us on API calls in the vast majority of cases.
|
|
|
|
Lastly, this code introduces the concept of error handling in Horizon.
|
|
The :func:`horizon.exceptions.handle` function is a centralized error
|
|
handling mechanism that takes all the guess-work and inconsistency out of
|
|
dealing with exceptions from the API. Use it everywhere.
|
|
|
|
Tying it together in a view
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
There are lots of pre-built class-based views in Horizon. We try to provide
|
|
starting points for all the common combinations of components.
|
|
|
|
In this case we want a starting view type that works with both tabs and
|
|
tables... that'd be the :class:`~horizon.tabs.TabbedTableView` class. It takes
|
|
the best of the dynamic delayed-loading capabilities tab groups provide and
|
|
mixes in the actions and AJAX-updating that tables are capable of with almost
|
|
no work on the user's end. Let's see what the code would look like::
|
|
|
|
from .tables import FlockingInstancesTable
|
|
from .tabs import FlockingTabs
|
|
|
|
class IndexView(tabs.TabbedTableView):
|
|
tab_group_class = FlockingTabs
|
|
table_class = FlockingInstancesTable
|
|
template_name = 'visualizations/flocking/index.html'
|
|
|
|
That would get us 100% of the way to what we need if this particular
|
|
demo didn't involve an extra AJAX call to fetch back our visualization
|
|
data via AJAX. Because of that we need to override the class' ``get()``
|
|
method to return the right data for an AJAX call::
|
|
|
|
from .tables import FlockingInstancesTable
|
|
from .tabs import FlockingTabs
|
|
|
|
class IndexView(tabs.TabbedTableView):
|
|
tab_group_class = FlockingTabs
|
|
table_class = FlockingInstancesTable
|
|
template_name = 'visualizations/flocking/index.html'
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
if self.request.is_ajax() and self.request.GET.get("json", False):
|
|
try:
|
|
instances = utils.get_instances_data(self.request)
|
|
except:
|
|
instances = []
|
|
exceptions.handle(request,
|
|
_('Unable to retrieve instance list.'))
|
|
data = json.dumps([i._apiresource._info for i in instances])
|
|
return http.HttpResponse(data)
|
|
else:
|
|
return super(IndexView, self).get(request, *args, **kwargs)
|
|
|
|
In this instance, we override the ``get()`` method such that if it's an
|
|
AJAX request and has the GET parameter we're looking for, it returns our
|
|
instance data in JSON format; otherwise it simply returns the view function
|
|
as per the usual.
|
|
|
|
The template
|
|
~~~~~~~~~~~~
|
|
|
|
We need three templates here: one for the view, and one for each of our two
|
|
tabs. The view template (in this case) can inherit from one of the other
|
|
dashboards::
|
|
|
|
{% extends 'base.html' %}
|
|
{% load i18n %}
|
|
{% block title %}{% trans "Flocking" %}{% endblock %}
|
|
|
|
{% block page_header %}
|
|
{% include "horizon/common/_page_header.html" with title=_("Flocking") %}
|
|
{% endblock page_header %}
|
|
|
|
{% block main %}
|
|
<div class="row-fluid">
|
|
<div class="span12">
|
|
{{ tab_group.render }}
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
This gives us a custom page title, a header, and render our tab group provided
|
|
by the view.
|
|
|
|
For the tabs, the one using the table is handled by a reusable template,
|
|
``"horizon/common/_detail_table.html"``. This is appropriate for any tab that
|
|
only displays a single table.
|
|
|
|
The second tab is a bit of secret sauce for the visualization, but it's still
|
|
quite simple and can be investigated in the github example.
|
|
|
|
The takeaway here is that each tab needs a template associated with it.
|
|
|
|
With all our code in place, the only thing left to do is to integrated it into
|
|
our OpenStack Dashboard site.
|
|
|
|
Setting up a project
|
|
====================
|
|
|
|
The vast majority of people will just customize the OpenStack Dashboard
|
|
example project that ships with Horizon. As such, this tutorial will
|
|
start from that and just illustrate the bits that can be customized.
|
|
|
|
Structure
|
|
---------
|
|
|
|
A site built on Horizon takes the form of a very typical Django project::
|
|
|
|
site/
|
|
|--__init__.py
|
|
|--manage.py
|
|
|--demo_dashboard/
|
|
|--__init__.py
|
|
|--models.py # required for Django even if unused
|
|
|--settings.py
|
|
|--templates/
|
|
|--static/
|
|
|
|
The key bits here are that ``demo_dashboard`` is on our python path, and that
|
|
the ``settings.py`` file here will contain our customized Horizon config.
|
|
|
|
The settings file
|
|
-----------------
|
|
|
|
There are several key things you will generally want to customize in your
|
|
site's settings file: specifying custom dashboards and panels, catching your
|
|
client's exception classes, and (possibly) specifying a file for advanced
|
|
overrides.
|
|
|
|
Specifying dashboards
|
|
~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Adding your own dashboard is as simple as creating a file in the
|
|
``openstack_dashboard/local/enabled`` directory named ``_50_visualizations.py``.
|
|
The contents of this file should resemble::
|
|
|
|
DASHBOARD = 'visualizations'
|
|
DEFAULT = True
|
|
ADD_EXCEPTIONS = {}
|
|
ADD_INSTALLED_APPS = ['openstack_dashboard.dashboards.visualizations']
|
|
|
|
.. seealso::
|
|
|
|
For more information on the significance of the file naming and an
|
|
explanation of the contents, check out
|
|
:doc:`Pluggable Settings for Dashboards </topics/settings>`
|
|
|
|
In this case, we've added our ``visualizations`` dashboard to the list of
|
|
dashboards to load. Note that the name here is the name of the dashboard's
|
|
module on the python path. It will find our ``dashboard.py`` file inside of
|
|
it and load both the dashboard and its panels automatically from there.
|
|
|
|
Error handling
|
|
~~~~~~~~~~~~~~
|
|
|
|
Adding custom error handler for your API client is quite easy. While it's not
|
|
necessary for this example, it would be done by customizing the ``ADD_EXCEPTIONS``
|
|
dictionary in the file added to ``openstack/local/enabled``::
|
|
|
|
import my_api.exceptions as my_api
|
|
|
|
ADD_EXCEPTIONS: {
|
|
'recoverable': [my_api.Error, my_api.ClientConnectionError],
|
|
'not_found': [my_api.NotFound],
|
|
'unauthorized': [my_api.NotAuthorized]
|
|
}
|
|
|
|
.. _overrides:
|
|
|
|
Override file
|
|
~~~~~~~~~~~~~
|
|
|
|
The override file is the "god-mode" dashboard editor. The hook for this file
|
|
sits right between the automatic discovery mechanisms and the final setup
|
|
routines for the entire site. By specifying an override file you can alter
|
|
any behavior you like in existing code. This tutorial won't go in-depth,
|
|
but let's just say that with great power comes great responsibility.
|
|
|
|
To specify an override file, you set the ``'customization_module'`` value in
|
|
the ``HORIZON_CONFIG`` dictionary to the dotted python path of your
|
|
override module::
|
|
|
|
HORIZON_CONFIG = {
|
|
'customization_module': 'demo_dashboard.overrides'
|
|
}
|
|
|
|
This file is capable of adding dashboards, adding panels to existing
|
|
dashboards, renaming existing dashboards and panels (or altering other
|
|
attributes on them), removing panels from existing dashboards, and so on.
|
|
|
|
We could say more, but it only gets more dangerous...
|
|
|
|
Conclusion
|
|
==========
|
|
|
|
Sadly, the cake was a lie. The information in this "tutorial" was never
|
|
meant to leave you with a working dashboard. It's close. But there's
|
|
waaaaaay too much javascript involved in the visualization to cover it all
|
|
here, and it'd be irrelevant to Horizon anyway.
|
|
|
|
If you want to see the finished product, check out the github example
|
|
referenced at the beginning of this tutorial.
|
|
|
|
Clone the repository and simply run ``./run_tests.sh --runserver``. That'll
|
|
give you a 100% working dashboard that uses every technique in this tutorial.
|
|
|
|
What you've learned here, however, is the fundamentals of almost everything
|
|
you need to know to start writing interfaces for your own project based on the
|
|
components Horizon provides.
|
|
|
|
If you have questions, or feedback on how this tutorial could be improved,
|
|
please feel free to pass them along!
|