1.. _install-customizing: 2 3=================== 4Customizing Horizon 5=================== 6 7.. seealso:: 8 9 You may also be interested in :doc:`themes` and :doc:`branding`. 10 11Changing the Site Title 12======================= 13 14The OpenStack Dashboard Site Title branding (i.e. "**OpenStack** Dashboard") 15can be overwritten by adding the attribute ``SITE_BRANDING`` 16to ``local_settings.py`` with the value being the desired name. 17 18The file ``local_settings.py`` can be found at the Horizon directory path of 19``openstack_dashboard/local/local_settings.py``. 20 21Changing the Brand Link 22======================= 23 24The logo also acts as a hyperlink. The default behavior is to redirect to 25``horizon:user_home``. By adding the attribute ``SITE_BRANDING_LINK`` with 26the desired url target e.g., ``http://sample-company.com`` in 27``local_settings.py``, the target of the hyperlink can be changed. 28 29Customizing the Footer 30====================== 31 32It is possible to customize the global and login footers by using Django's 33recursive inheritance to extend the ``base.html``, ``auth/login.html``, and 34``auth/_login_form.html`` templates. You do this by naming your template the 35same name as the template you wish to extend and only overriding the blocks you 36wish to change. 37 38Your theme's ``base.html``:: 39 40 {% extends "base.html" %} 41 42 {% block footer %} 43 <p>My custom footer</p> 44 {% endblock %} 45 46Your theme's ``auth/login.html``:: 47 48 {% extends "auth/login.html" %} 49 50 {% block footer %} 51 <p>My custom login footer</p> 52 {% endblock %} 53 54Your theme's ``auth/_login_form.html``:: 55 56 {% extends "auth/_login_form.html" %} 57 58 {% block login_footer %} 59 {% comment %} 60 You MUST have block.super because that includes the login button. 61 {% endcomment %} 62 {{ block.super }} 63 <p>My custom login form footer</p> 64 {% endblock %} 65 66See the ``example`` theme for a working theme that uses these blocks. 67 68 69Modifying Existing Dashboards and Panels 70======================================== 71 72If you wish to alter dashboards or panels which are not part of your codebase, 73you can specify a custom python module which will be loaded after the entire 74Horizon site has been initialized, but prior to the URLconf construction. 75This allows for common site-customization requirements such as: 76 77* Registering or unregistering panels from an existing dashboard. 78* Changing the names of dashboards and panels. 79* Re-ordering panels within a dashboard or panel group. 80 81Default Horizon panels are loaded based upon files within the 82openstack_dashboard/enabled/ folder. These files are loaded based upon the 83filename order, with space left for more files to be added. There are some 84example files available within this folder, with the .example suffix 85added. Developers and deployers should strive to use this method of 86customization as much as possible, and support for this is given preference 87over more exotic methods such as monkey patching and overrides files. 88 89.. _horizon-customization-module: 90 91Horizon customization module (overrides) 92======================================== 93 94Horizon has a global overrides mechanism available to perform customizations 95that are not yet customizable via configuration settings. This file can perform 96monkey patching and other forms of customization which are not possible via the 97enabled folder's customization method. 98 99This method of customization is meant to be available for deployers of Horizon, 100and use of this should be avoided by Horizon plugins at all cost. Plugins 101needing this level of monkey patching and flexibility should instead look for 102changing their __init__.py file and performing customizations through other 103means. 104 105To specify the python module containing your modifications, add the key 106``customization_module`` to your ``HORIZON_CONFIG`` dictionary in 107``local_settings.py``. The value should be a string containing the path to your 108module in dotted python path notation. Example:: 109 110 HORIZON_CONFIG["customization_module"] = "my_project.overrides" 111 112You can do essentially anything you like in the customization module. For 113example, you could change the name of a panel:: 114 115 from django.utils.translation import ugettext_lazy as _ 116 117 import horizon 118 119 # Rename "User Settings" to "User Options" 120 settings = horizon.get_dashboard("settings") 121 user_panel = settings.get_panel("user") 122 user_panel.name = _("User Options") 123 124Or get the instances panel:: 125 126 projects_dashboard = horizon.get_dashboard("project") 127 instances_panel = projects_dashboard.get_panel("instances") 128 129Or just remove it entirely:: 130 131 projects_dashboard.unregister(instances_panel.__class__) 132 133You cannot unregister a ``default_panel``. If you wish to remove a 134``default_panel``, you need to make a different panel in the dashboard as a 135``default_panel`` and then unregister the former. For example, if you wished 136to remove the ``overview_panel`` from the ``Project`` dashboard, you could do 137the following:: 138 139 project = horizon.get_dashboard('project') 140 project.default_panel = "instances" 141 overview = project.get_panel('overview') 142 project.unregister(overview.__class__) 143 144You can also override existing methods with your own versions:: 145 146 from openstack_dashboard.dashboards.admin.info import tabs 147 from openstack_dashboard.dashboards.project.instances import tables 148 149 NO = lambda *x: False 150 151 tables.AssociateIP.allowed = NO 152 tables.SimpleAssociateIP.allowed = NO 153 tables.SimpleDisassociateIP.allowed = NO 154 155You could also customize what columns are displayed in an existing 156table, by redefining the ``columns`` attribute of its ``Meta`` 157class. This can be achieved in 3 steps: 158 159#. Extend the table that you wish to modify 160#. Redefine the ``columns`` attribute under the ``Meta`` class for this 161 new table 162#. Modify the ``table_class`` attribute for the related view so that it 163 points to the new table 164 165 166For example, if you wished to remove the Admin State column from the 167:class:`~openstack_dashboard.dashboards.admin.networks.tables.NetworksTable`, 168you could do the following:: 169 170 from openstack_dashboard.dashboards.project.networks import tables 171 from openstack_dashboard.dashboards.project.networks import views 172 173 class MyNetworksTable(tables.NetworksTable): 174 175 class Meta(tables.NetworksTable.Meta): 176 columns = ('name', 'subnets', 'shared', 'status') 177 178 views.IndexView.table_class = MyNetworksTable 179 180If you want to add a column you can override the parent table in a 181similar way, add the new column definition and then use the ``Meta`` 182``columns`` attribute to control the column order as needed. 183 184.. NOTE:: 185 186 ``my_project.overrides`` needs to be importable by the python process running 187 Horizon. 188 If your module is not installed as a system-wide python package, 189 you can either make it installable (e.g., with a setup.py) 190 or you can adjust the python path used by your WSGI server to include its location. 191 192 Probably the easiest way is to add a ``python-path`` argument to 193 the ``WSGIDaemonProcess`` line in Apache's Horizon config. 194 195 Assuming your ``my_project`` module lives in ``/opt/python/my_project``, 196 you'd make it look like the following:: 197 198 WSGIDaemonProcess [... existing options ...] python-path=/opt/python 199 200 201Customize the project and user table columns 202============================================ 203 204 205Keystone V3 has a place to store extra information regarding project and user. 206Using the override mechanism described in :ref:`horizon-customization-module`, 207Horizon is able to show these extra information as a custom column. 208For example, if a user in Keystone has an attribute ``phone_num``, you could 209define new column:: 210 211 from django.utils.translation import ugettext_lazy as _ 212 213 from horizon import forms 214 from horizon import tables 215 216 from openstack_dashboard.dashboards.identity.users import tables as user_tables 217 from openstack_dashboard.dashboards.identity.users import views 218 219 class MyUsersTable(user_tables.UsersTable): 220 phone_num = tables.Column('phone_num', 221 verbose_name=_('Phone Number'), 222 form_field=forms.CharField(),) 223 224 class Meta(user_tables.UsersTable.Meta): 225 columns = ('name', 'description', 'phone_num') 226 227 views.IndexView.table_class = MyUsersTable 228 229 230Customize Angular dashboards 231============================ 232 233In Angular, you may write a plugin to extend certain features. Two components 234in the Horizon framework that make this possible are the extensibility service 235and the resource type registry service. The ``extensibleService`` allows 236certain Horizon elements to be extended dynamically, including add, remove, and 237replace. The ``resourceTypeRegistry`` service provides methods to set and get 238information pertaining to a resource type object. We use Heat type names like 239``OS::Glance::Image`` as our reference name. 240 241Some information you may place in the registry include: 242 243* API to fetch data from 244* Property names 245* Actions (e.g. "Create Volume") 246* URL paths to detail view or detail drawer 247* Property information like labels or formatting for property values 248 249These properties in the registry use the extensibility service (as of Newton 250release): 251 252* globalActions 253* batchActions 254* itemActions 255* detailViews 256* tableColumns 257* filterFacets 258 259Using the information from the registry, we can build out our dashboard panels. 260Panels use the high-level directive ``hzResourceTable`` that replaces common 261templates so we do not need to write boilerplate HTML and controller code. It 262gives developers a quick way to build a new table or change an existing table. 263 264.. note:: 265 266 You may still choose to use the HTML template for complete control of form 267 and functionality. For example, you may want to create a custom footer. 268 You may also use the ``hzDynamicTable`` directive (what ``hzResourceTable`` 269 uses under the hood) directly. However, neither of these is extensible. 270 You would need to override the panel completely. 271 272This is a sample module file to demonstrate how to make some customizations to 273the Images Panel.:: 274 275 (function() { 276 'use strict'; 277 278 angular 279 .module('horizon.app.core.images') 280 .run(customizeImagePanel); 281 282 customizeImagePanel.$inject = [ 283 'horizon.framework.conf.resource-type-registry.service', 284 'horizon.app.core.images.basePath', 285 'horizon.app.core.images.resourceType', 286 'horizon.app.core.images.actions.surprise.service' 287 ]; 288 289 function customizeImagePanel(registry, basePath, imageResourceType, surpriseService) { 290 // get registry for ``OS::Glance::Image`` 291 registry = registry.getResourceType(imageResourceType); 292 293 // replace existing Size column to make the font color red 294 var column = { 295 id: 'size', 296 priority: 2, 297 template: '<a style="color:red;">{$ item.size | bytes $}</a>' 298 }; 299 registry.tableColumns.replace('size', column); 300 301 // add a new detail view 302 registry.detailsViews 303 .append({ 304 id: 'anotherDetailView', 305 name: gettext('Another Detail View'), 306 template: basePath + 'demo/detail.html' 307 }); 308 309 // set a different summary drawer template 310 registry.setSummaryTemplateUrl(basePath + 'demo/drawer.html'); 311 312 // add a new global action 313 registry.globalActions 314 .append({ 315 id: 'surpriseAction', 316 service: surpriseService, 317 template: { 318 text: gettext('Surprise') 319 } 320 }); 321 } 322 })(); 323 324Additionally, you should have content defined in ``detail.html`` and 325``drawer.html``, as well as define the ``surpriseService`` which is based off 326the ``actions`` directive and needs allowed and perform methods defined. 327 328 329Icons 330===== 331 332Horizon uses font icons from Font Awesome. Please see `Font Awesome`_ for 333instructions on how to use icons in the code. 334 335To add icon to Table Action, use icon property. Example: 336 337.. code-block:: python 338 339 class CreateSnapshot(tables.LinkAction): 340 name = "snapshot" 341 verbose_name = _("Create Snapshot") 342 icon = "camera" 343 344Additionally, the site-wide default button classes can be configured by 345setting ``ACTION_CSS_CLASSES`` to a tuple of the classes you wish to appear 346on all action buttons in your ``local_settings.py`` file. 347 348 349Custom Stylesheets 350================== 351 352It is possible to define custom stylesheets for your dashboards. Horizon's base 353template ``openstack_dashboard/templates/base.html`` defines multiple blocks 354that can be overridden. 355 356To define custom css files that apply only to a specific dashboard, create 357a base template in your dashboard's templates folder, which extends Horizon's 358base template e.g. ``openstack_dashboard/dashboards/my_custom_dashboard/ 359templates/my_custom_dashboard/base.html``. 360 361In this template, redefine ``block css``. (Don't forget to include 362``_stylesheets.html`` which includes all Horizon's default stylesheets.):: 363 364 {% extends 'base.html' %} 365 366 {% block css %} 367 {% include "_stylesheets.html" %} 368 369 {% load compress %} 370 {% compress css %} 371 <link href='{{ STATIC_URL }}my_custom_dashboard/scss/my_custom_dashboard.scss' type='text/scss' media='screen' rel='stylesheet' /> 372 {% endcompress %} 373 {% endblock %} 374 375The custom stylesheets then reside in the dashboard's own ``static`` folder 376``openstack_dashboard/dashboards/my_custom_dashboard/static/my_custom_dashboard/scss/my_custom_dashboard.scss``. 377 378All dashboard's templates have to inherit from dashboard's base.html:: 379 380 {% extends 'my_custom_dashboard/base.html' %} 381 ... 382 383 384Custom Javascript 385================= 386 387Similarly to adding custom styling (see above), it is possible to include 388custom javascript files. 389 390All Horizon's javascript files are listed in the 391``openstack_dashboard/templates/horizon/_scripts.html`` 392partial template, which is included in Horizon's base template in ``block js``. 393 394To add custom javascript files, create an ``_scripts.html`` partial template in 395your dashboard 396``openstack_dashboard/dashboards/my_custom_dashboard/templates/my_custom_dashboard/_scripts.html`` 397which extends ``horizon/_scripts.html``. In this template override the 398``block custom_js_files`` including your custom javascript files:: 399 400 {% extends 'horizon/_scripts.html' %} 401 402 {% block custom_js_files %} 403 <script src='{{ STATIC_URL }}my_custom_dashboard/js/my_custom_js.js' type='text/javascript' charset='utf-8'></script> 404 {% endblock %} 405 406 407In your dashboard's own base template ``openstack_dashboard/dashboards/ 408my_custom_dashboard/templates/my_custom_dashboard/base.html`` override 409``block js`` with inclusion of dashboard's own ``_scripts.html``:: 410 411 {% block js %} 412 {% include "my_custom_dashboard/_scripts.html" %} 413 {% endblock %} 414 415The result is a single compressed js file consisting both Horizon and 416dashboard's custom scripts. 417 418Custom Head js 419-------------- 420 421Additionally, some scripts require you to place them within the page's <head> 422tag. To do this, recursively extend the ``base.html`` template in your theme 423to override the ``custom_head_js`` block. 424 425Your theme's ``base.html``:: 426 427 {% extends "base.html" %} 428 429 {% block custom_head_js %} 430 <script src='{{ STATIC_URL }}/my_custom_dashboard/js/my_custom_js.js' type='text/javascript' charset='utf-8'></script> 431 {% endblock %} 432 433See the ``example`` theme for a working theme that uses these blocks. 434 435.. warning:: 436 437 Don't use the ``custom_head_js`` block for analytics tracking. See below. 438 439Custom Analytics 440---------------- 441 442For analytics or tracking scripts you should avoid the ``custom_head_js`` 443block. We have a specific block instead called ``custom_analytics``. Much like 444the ``custom_head_js`` block this inserts additional content into the head of 445the ``base.html`` template and it will be on all pages. 446 447The reason for an analytics specific block is that for security purposes we 448want to be able to turn off tracking on certain pages that we deem sensitive. 449This is done for the safety of the users and the cloud admins. By using this 450block instead, pages using ``base.html`` can override it themselves when they 451want to avoid tracking. They can't simply override the custom js because it may 452be non-tracking code. 453 454Your theme's ``base.html``:: 455 456 {% extends "base.html" %} 457 458 {% block custom_analytics %} 459 <script src='{{ STATIC_URL }}/my_custom_dashboard/js/my_tracking_js.js' type='text/javascript' charset='utf-8'></script> 460 {% endblock %} 461 462See the ``example`` theme for a working theme that uses these blocks. 463 464Customizing Meta Attributes 465=========================== 466 467To add custom metadata attributes to your project's base template use the 468``custom_metadata`` block. To do this, recursively extend the ``base.html`` 469template in your theme to override the ``custom_metadata`` block. The contents 470of this block will be inserted into the page's <head> just after the default 471Horizon meta tags. 472 473Your theme's ``base.html``:: 474 475 {% extends "base.html" %} 476 477 {% block custom_metadata %} 478 <meta name="description" content="My custom metadata."> 479 {% endblock %} 480 481See the ``example`` theme for a working theme that uses these blocks. 482 483.. _Font Awesome: https://fortawesome.github.io/Font-Awesome/ 484