Contents:
"Think simple" as my old master used to say - meaning reduce the whole of its parts into the simplest terms, getting back to first principles.
—Frank Lloyd Wright
In order for Horizon (OpenStack's dashboard project) to be a viable long-term solution it needs to meet several key values:
- Core Support: Out-of-the-box support for all core OpenStack projects.
- Extensible: Anyone can add a new component as a "first-class citizen".
- Manageable: The core codebase should be simple and easy-to-navigate.
- Consistent: Visual and interaction paradigms are maintained throughout.
- Stable: A reliable API with an emphasis on backwards-compatibility.
- Usable: Providing an awesome interface that people want to use.
The only way to attain and uphold those ideals is to define an architectural strategy which makes it easy for the developers to implement those values.
Horizon started life as a single app to manage OpenStack's compute project. As such, all it needed was a set of views, templates, and API calls.
From there it grew to support multiple OpenStack projects and APIs gradually, arranged rigidly into "dash" and "syspanel" groupings.
Finally a first-attempt plugin system was added using signals to add additional URL patterns and links to into the dash and syspanel framework.
This incremental growth has served the goal of "Core Support" phenomenally, but has left "Extensible" and "Manageable" out. The other key values are all taking shape of their own accord but can be further supported with a new architectural understanding.
Note
See the Frequently Asked Questions section for clarification of some of these high-level concepts.
At it's core, Horizon should be a registration pattern for applications to hook into. Here's what that means and how it's implemented in terms of our values:
Horizon will ship with the existing "User Dashboard" and "Syspanel" modules reconstituted as applications which register themselves with Horizon.
The Horizon application will also ship with a set of API abstractions
for the core OpenStack projects in order to provide a consistent, stable set
of reusable common methods. This would just be a refactor of the existing
api.py
file into individual files per service.
A Horizon application will be based around a Dashboard
class that
provides a constistent API and set of capabilities for both core OpenStack
apps shipped with Horizon and third-party apps. The Dashboard
class will
be treated as a top-level navigation item--as "dash" and "syspanel" are
currently.
Should a developer wish to provide functionality within an existing dashboard (e.g. adding a monitoring panel to the user dashboard) the simple registration patterns make it possible to write an app which hooks into other dashboards just as easily as creating a new dashboard. All you have to do is import the dashboard you wish to modify.
Within the application, there will be a simple method for registering
"panels" (sub-navigation items). Each panel will contain all the necessary
logic (views, forms, tests, etc.) for that interface. This granular breakdown
prevents files (such as api.py
) from becoming thousands of lines long and
makes code easy to find by correlating it directly to the navigation.
By providing the necessary core classes to build from, as well as a solid set of reusable templates and additional tools (base form classes, base widget classes, template tags, and perhaps even base class-based views) we can maintain consistency across applications.
By architecting around these core classes and reusable components we create an implicit contract that changes to these components will be made in the most backwards-compatible ways whenever possible.
Ultimately that's up to each and every developer that touches the code, but if we get all the other goals out of the way then we are free to focus on the best possible experience.
At the project level you add Horizon and any desired dashboards to your
settings.INSTALLED_APPS
:
INSTALLED_APPS = ( 'django', ... 'horizon', 'horizon.dash', 'horizon.syspanel', 'hardware_dashboard', # This is a custom dashboard application 'my_standalone_monitoring_panel', # This adds a panel to a dashboard )
Then you add a single line to your project's urls.py
:
url(r'', include(horizon.urls)),
Those urls are automatically constructed based on the registered Horizon apps. If a different URL structure is desired it can be constructed by hand.
Pre-built template tags would generate navigation. In your nav.html
template you might have the following:
{% load horizon %} <div class='nav'> {% horizon_main_nav %} </div>
And in your sidebar.html
you might have:
{% load horizon %} <div class='sidebar'> {% horizon_dashboard_nav %} </div>
The dashboard nav could either implicitly render the panel navigation based on a current "active" dashboard in the template context, or it could be explicitly passed a dashboard as an argument.
An application would have the following structure (we'll use syspanel as an example):
syspanel/ |---__init__.py |---dashboard.py <-----Registers the app with Horizon and sets dashboard properties |---templates/ |---templatetags/ |---overview/ |---services/ |---images/ |---__init__.py |---panel.py <-----Registers the panel in the app and defines panel properties |---urls.py |---views.py |---forms.py |---tests.py |---api.py <-------Optional additional API methods for non-core services |---templates/ ... ...
Inside of dashboard.py
you would have a class definition and the registration
process:
import horizon class Syspanel(horizon.Dashboard): name = "Syspanel" # Appears in navigation panels = ('overview', 'services', 'instances', 'flavors', 'images', 'tenants', 'users', 'quotas',) # Optional to control ordering default_panel = 'overview' roles = ('admin',) # Provides RBAC at the dashboard-level ... # Other common attributes horizon.register(Syspanel)
By default the Dashboard
class would include any Panel
obejcts that are
registered with them, as in a panels.py
:
import horizon from syspanel import dashboard class Images(horizon.Panel): name = "Images" urls = 'syspanel.images.urls' roles = ('admin', 'sysadmin',) # Fine-grained RBAC per-panel # You could also register your panel with another application's dashboard dashboard.register(Images)
The Panel.urls
attribute allows the rollup of url patterns from panels
to dashboards to Horizon, resulting in a wholly extensible, configurable URL
structure.
The really hard problem lies in adding multi-step workflows that may involve steps from different panels and different apps. These steps may even have different access controls, which leads to the pathological case of a user who can complete all of a workflow except the last step and is left furious.
To solve this problem, Horizon would also provide a Workflow
class that
handles the high-level definition of the workflow and can be hooked into a
panel's URLconf much like Django's class-based views. It would maintain
a session-based state for managing data across steps.
Here is a rough usage example:
from django.contrib import messages from django.core.urlresolvers import reverse import horizon class UploadAndLaunchImage(horizon.Workflow): """ A two-step workflow that allows the user to upload an image and launch it. """ # List ALL roles required to complete the workflow roles = ('member', 'admin', 'sysadmin',) # Views that are the steps of the workflow (in order) views = ('syspanel.images.upload', 'dash.images.launch',) def complete(self): # What to do upon successful completion of the workflow return reverse('success_url') def cancel(self): # Any cleanup that needs to happen for a partial completion if self.state['uploaded_image']: self.state['uploaded_image'].delete() return reverse('cancel_url')
In your panel's urls.py
you would then include the following:
from syspanel.images import workflows patterns = ('', url(r'^upload_and_launch/$', workflows.UploadAndLaunchImage.as_view(), name='upload_and_launch'), )
This way the workflow is picked up in the panel's normal URL structure.
What is the relationship between Dashboards
, Panels
, and navigation?
The navigational structure would be strongly encouraged to flow from
Dashboard
objects as top-level navigation items toPanel
objects as sub-navigation items as in the current implementation. Template tags would be provided to automatically generate this structure.That said, you are not required to use the provided tools and can write templates and URLconfs by hand with any desired structure.
Does a panel have to be an app in INSTALLED_APPS
?
A panel can live in any Python module. It can be a standalone which ties
into an existing dashboard, or it can be contained alongside others within
a larger dashboard "app". There is no strict enforcement here. Python
is "a language for consenting adults." An module containing a Panel does
not need to be added to INSTALLED_APPS
, but this would be a common
way to load a standalone panel.
Could I hook an external service into a panel using, for example, an iFrame?
Panels are just entry-points to hook views into the larger dashboard navigational structure and enforce common attributes like RBAC. The view and corresponding templates can contain anything you would like, including iFrames.
What does this mean for visual design?
The ability to add an arbitrary number of top-level navigational items
(Dashboard
objects) poses a new design challenge. Designer Paul Tashima
has taken on the challenge of providing a reference design for Horizon
which supports this possibility.
Horizon
The OpenStack dashboard project. Also the name of the top-level Python singleton which handles registration for the app.
Dashboard
A Python class representing a top-level navigation item (e.g. "syspanel") which provides a consistent API for Horizon-compatible applications.
Panel
A Python class representing a sub-navigation item (e.g. "instances") which contains all the necessary logic (views, forms, tests, etc.) for that interface.
Workflow
A Python class which defines a multi-step set of tasks which may span multiple panels, dashboards or apps and may require multiple access controls.