Theming — Adapting ReportServer's UI

With ReportServer 3.0 Enterprise Edition we have added capabilities to style the user interface to allow to adapt the look and feel to match your corporate identity. In this tutorial we want to show how to develop a new theme and provide some helpers that make theme development easier.

The Configuration File theme.cf

The ReportServer theme is controlled via the configuration file /etc/ui/theme.cf which in its basic form looks something like:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <theme type="default">
    
    <logo>
     <!-- define which logos to use -->
    </logo>
    
    <colors>
     <!-- define names for colors to be used -->     
    </colors>
    
    <colorMapping>
     <!-- define mapping of colors to elements -->     
    </colorMapping>
    
    <css>
     <!-- define additional css rules -->     
    </css>
  
  </theme>
</configuration>

The <theme> element has a single attribute type which controls the ReportServer base theme that is loaded. Currently two base themes are available which are called:

default
The standard ReportServer theme.
borders
The standard theme with some additional borders.

The <logo> tags allow to define the logos that are used for the login screen, the logo on the top left of the module bar as well as a logo to be used in the report documentation that is displayed in the TeamSpace. Let's consider the following directive:

   <logo>
      <login>
	  <html><![CDATA[<b>THE LOGIN LOGO</b>]]></html>
    	  <width>200px</width>
      </login>
      <header>
          <html><![CDATA[<b>THE HEADER LOGO</b>]]></html>
    	  <width>185px</width>
      </header>
      <report>Some URI pointing to a Logo to be used in the report documentation</report>
    </logo>

The first directive (logo.login) allows to specify an HTML snippet to be used on the login page. The width defines the width for the resulting html element. The header directive, on the other hand, controls the logo in the top left corner after login. Finally, the <report> tag can contain a URI that points to an image to be used within the report documentation.

Remember that when changing the config you need to issue the config reload command on the terminal (press CTRL+ALT+T to open the terminal) for the changes to take effect.

The <colors> and <colorMapping> tags make up the main part of a new theme. Here you can define colors (within the <colors> tag) and then assign these to various elements (within the <colorMapping> tag). A simple color definition could be

<color name="white" color="#FFFFFF"/>

This could then be assigned to the background via

<map useFor="body.bg" colorRef="white"/>

which assigns the color white to any element that uses body.bg as its color. The resulting theme.cf would then be:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <theme type="default">
    
    <logo>
     <!-- define which logos to use -->
    </logo>
    
    <colors>
     <!-- define names for colors to be used -->     
     <color name="white" color="#FFFFFF"/>
    </colors>
    
    <colorMapping>
     <!-- define mapping of colors to elements -->  
     <map useFor="body.bg" colorRef="white"/>   
    </colorMapping>
    
    <css>
     <!-- define additional css rules -->     
    </css>
  
  </theme>
</configuration>

Besides defining a color mapping by referencing a previously defined color, you can also directly specify a color via the color attribute. A second alternative is to set the mapping of one element group to follow another element group. For this use the sameAs attribute.

    <colorMapping>
     <!-- define mapping of colors to elements -->  
     <map useFor="body.bg" colorRef="white"/>   
     <map useFor="tbar.btn.bg" sameAs="body.bg"/>        
     <map useFor="terminal.text" color="#00B000"/>             
    </colorMapping>

If we can change the background color via assigning a custom color to body.bg this immediately raises the question, which element groups can we assign colors to? To get a list of the currently available element groups, you can look through ReportServer's css (and scan for css comments /*col:NAME*/) or use the following script:

import net.datenwerke.gf.service.theme.ThemeService

new TreeMap(GLOBALS.getInstance(ThemeService).colorMap).each{ k,v ->
  tout.println "$k: $v"
}

null

The script is available here.

Currently, this would tell us that the following groups are available (here with an added description):

Element Group Color Color Code Description
bg #B8BDC0The background.
text #000000Text when on background (bg).
bg.light #FFFFFFLight variant of background. For example used as background of panels.
light.text #000000Text on light background.
bg.shaded #EEEEEEA shaded variant of the background. Used, for example, as the background for toolbars.
shaded.text #666666Text on shaded background.
bg.dark #6D708BA darker variant of the background color.
border.light #B8BDC0A color used for (thin) borders on light background.
header.bg #132834The background of the top module bar (the header).
header.text.active #FFFFFFText color of active modules and logo.
header.text.inactive #B8BDC0Text color of inactive modules.
header.text.right #B8BDC0Text color of user name and profile.
hl.dark.bg #3E4059A dark highlight color.
hl.dark.text #FFFFFFText on the dark highlight.
hl.light.bg #DFE0EBA lighter highlight color.
hl.light.text #000000Text on the lighter highlight color.
tbar.btn.bg #B8BDC0Background color of buttons in toolbars.
terminal.bg #000000Background of the terminal.
terminal.hl.bg #6D708Bhighlighted background of the terminal.
terminal.link #FFFFFFLinks on the terminal.
terminal.text #00B000Standard text on the terminal.

We have tried to group together elements with a similar exposure to make theming easier. At the extreme we could have given a name to every element, which, while yielding complete flexibility, would make theming much harder. However, we'd be happy to receive feedback if the above grouping causes you problems. Note that even though our grouping is not too granular, you can easily dive into the css and overwrite the styles of any specific element.

This is what the last part of the config is for. If you are not familiar with CSS, simply skip the following paragraphs.

The <css> tags enclose any additional styles that you might want to add. These are added at the very end, thus allowing you to overwrite each and every single element if you wish. Consider, for example, the teamspace grid. Here, the grid header is using the dark highlight color. The CSS from the ReportServer's default stylesheet is:

.rs-teamspace-list .rs-grid-head {
  border: none !important;
  background: none repeat scroll 0 0 #3E4059 /*col:hl.dark.bg*/ !important;
}

.rs-teamspace-list .rs-grid-head td{
  border-bottom: none !important;
  border-color: #FFFFFF /*col:hl.dark.text*/ !important;
  color: #FFFFFF /*col:hl.dark.text*/ !important;
}

If we instead wanted to use a shaded background, we could add the following directive to the theme.cf config file

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <theme type="default">
    
    <logo>
     <!-- define which logos to use -->
    </logo>
    
    <colors>
     <!-- define names for colors to be used -->     
     <color name="white" color="#FFFFFF"/>
    </colors>
    
    <colorMapping>
     <!-- define mapping of colors to elements -->  
     <map useFor="body.bg" colorRef="white"/>   
    </colorMapping>
    
    <css>
     <!-- define additional css rules -->     
     .rs-teamspace-list .rs-grid-head {
	    background: none repeat scroll 0 0 #EEEEEE /*col:bg.shaded*/ !important;
     }

     .rs-teamspace-list .rs-grid-head td{
	    border-bottom: none !important;
	    border-color: #666666 /*col:shaded.text*/ !important;
	    color: #666666 /*col:shaded.text*/ !important;
     }
    </css>
  
  </theme>
</configuration>

There is one thing to note. In the above above directive the css comment /*col:NAME*/ denotes a marker for ReportServer what color to choose. If present and the config file contains a custom color for bg.shaded (or shaded.text) then ReportServer would also there overwrite the color. If you want to fix the color, simply remove the CSS comment.

So much for the config file. In theory, you can now start theming. However, we can make our lives even easier with a bit of scripting.

Automatic Reload of Config File

When changing the configuration, the changes are only picked up, after we issue a config reload on the terminal. In the following we will write little script that will do this automatically. For this, we will listen to change events on files and if we detect a change on file theme.cf then we'll trigger a reload of the config file. Let's have a look at the script (for an introduction to scripting see the Administration and Script guides.


						
						import net.datenwerke.rs.fileserver.service.fileserver.entities.FileServerFile
						import net.datenwerke.security.service.eventlogger.jpa.MergeEntityEvent
						import net.datenwerke.rs.utils.eventbus.EventHandler
						import net.datenwerke.rs.utils.config.ConfigService

						def HANDLER_NAME = "FORCE_THEME_RELOAD_HANDLER"

						def configServiceProvider = GLOBALS.getProvider(ConfigService.class)

						def callback = [
						   handle: { e ->
							  if(null == e || ! (e instanceof MergeEntityEvent))
								 return
							  def file = e.getObject();
							  if(! "theme.cf".equals(file.getName()))
								 return
	  
							  configServiceProvider.get().clearCache("ui/theme.cf")
						   }
						] as EventHandler

						GLOBALS.services.callbackRegistry.attachObjectEventHandler(HANDLER_NAME, MergeEntityEvent, FileServerFile, callback)
						

We are interested in change events on files (internally represented by net.datenwerke.rs.fileserver.­service.­fileserver.entities.FileServerFile objects). For this, we use the callbackRegistry in line 22 to attach an ObjectEventHandler for so called MergeEntityEvents (represented by class MergeEntityEvent.class).

ReportServer will throw events whenever an object is changed, and the above registration ensures that our script is called whenever an object of type FileServerFile is merged (i.e., changed). Now the callback itself implements the single method handle which takes one parameter, the event which should be of type MergeEntityEvent (as we only registered for these). Then in the remainder (lines 14 to 18) we simply check if the merged file is called theme.cf and if so we ask the ConfigService (loaded in line 8) to clear the cache for the theme config.

When registering to receive object events you need to ensure, that your code does not throw any exceptions, as these are executed within ReportServer's main thread and an error in this case would interrupt the storage of the changed file.

Now, if you execute the script then you will notice that after changing the configuration it is sufficient to reload the browser. The changes are then directly picked up and the new theme is loaded.

This already makes theme development a few clicks faster. In the next section we introduce one more helper, to make it a blast.

Reloading new Theme without Reloading Browser

Having to reload the browser is a bit tedious, so in this final part we are going to add a button to the toolbar on the theme.cf config file which instantly reloads the theme. Following is the completed script:


						import net.datenwerke.rs.scripting.service.scripting.extensions.*
						
						def service = GLOBALS.services['clientExtensionService']

						def entry = new AddToolbarEntryExtension()
						entry.setLabel("Reload theme")
						entry.setIcon("refresh")
						entry.setToolbarName("fileserver:admin:view:toolbar")
						entry.setJavaScript("""
							\$wnd.\$("#rs-the-theme").remove();
							\$wnd.\$('head').append( \$wnd.\$('<link id="rs-the-theme" rel="stylesheet" type="text/css" />').attr('href', 'reportserver/rstheme?' + Math.random() ) );
						""")

						entry.addDisplayCondition(
						   new DisplayCondition("path", "/fileserver/etc/ui/theme.cf")
						)
						
						service.addToolbarEntry(entry)
						

The script uses the ClientExtensionService (loaded in line 3) which allows to add buttons to various toolbars, or menus. In our case, we want to add a button to the toolbar in the FileServer when the theme.cf config file is active. For this we create an AddToolbarEntryExtension (lines 5 to 12) object which we give a label and an icon (choose any Font Awesome icon).

In line 8 we tell the extension that it should apply to the toolbar identified by fileserver:admin:view:toolbar (the file server's toolbar). Without any additional conditions, the button would now appear for every object in the toolbar. Thus, to only show it on file theme.cf we add a display condition and specify the specific path (lines 14-16).

The actual work (replacing the theme) is done when pressing the button and for this we use a little bit of javascript which we add to the entry in lines 10 and eleven. ReportServer's CSS is loaded into a link tag with the id rs-the-theme. So what we need is to remove it, and replace it with a fresh tag. For this we use jQuery. Removing the tag is done by

$("#rs-the-theme").remove();

This is what we do in line 10, or almost. The reason why the above does not work directly is that ReportServer is written in GWT and the javascript is run within the GWT context (i.e., a nested frame). Thus, to access the actual context and its window object we use the variable $wnd which is provided by GWT for this purpose, making the call.

$wnd.$("#rs-the-theme").remove();

Now noticing that $ is a special instruction in groovy strings, we need to escape the $-signs.

In line 11 we add a fresh link tag to the head element which reloads the theme. And with this we are at the end of this tutorial.

Happy Theming