Providing a PUC Perspective from Plugins
What is a perspective?
Perspectives are a specialized mode added to the Pentaho User Console (PUC) for the next release of the suite (SUGAR). Active development is still very much underway, but I wanted to highlight this really cool new feature. CI builds of Sugar are available at http://ci.pentaho.com/view/Sugar/.
A perspective changes the behavior and appearance of PUC by taking over certain areas of the interface. The PUC main toolbar and main menubar are now easily setup with XUL overlays. The content area of PUC can also be completely owned by a perspective. In this way, PUC can be dramatically customized. Switching perspectives is done by clicking on them in the upper right hand corner.
How to register a perspective
From an API standpoint, registering a perspective with the system simply means adding the right objects to the perspective manager. There are two interfaces of concern here, IPluginPerspective and IPluginPerspectiveManager. IPluginPerspectiveManager is added to pentahoObjects.spring.xml making it available through PentahoSystem. The easiest way to add a perspective to the system is to simply add its definition to the plugin.xml of a plugin. However, you are not constrained to this, you can register a new perspective through the API. For example,
IPluginPerspective perspective = new DefaultPluginPerspective(); perspective.setId(..); perspective.setOverlays(..); etc. IPluginPerspectiveManager manager = PentahoSystem.get(IPluginPerspectiveManager.class, getPentahoSession()); manager.addPluginPerspective(perspective);
Implementing the interfaces
Should you decide to implement your own perspective interfaces and replace ours, there are only a few interfaces to concern yourself with. The first thing you must do is replace the IPluginPerspectiveManager in pentahoObjects.spring.xml, for example:
<bean id="IPluginPerspectiveManager" class="com.yourcompany.BetterPerspectiveManager" scope="singleton" />
Once you've done this, and your class is available to the system your plugin perspective manager will be used to register perspectives. PUC will use PentahoSystem to use your manager to list the available perspectives. A perspective itself must extend IPluginPerspective, for example:
public class MyPluginPerspective extends IPluginPerspective { .. }
This class is just a bean and provides:
- id (unique perspective id)
- title (name of the perspective shown in PUC)
- content-url (the url of the page used to hijack PUC content area)
- resourcebundle (the uri to a message bundle for localizing the title of the perspective)
- overlays (xul overlays to apply to menu/toolbar of PUC)
- layout-priority (used to control the order which perspectives show up in PUC, BI Browser is -1)
- required-security-actions (action based security can be used to check if the user "isAllowed")
The easy way: How to define perspectives in plugin.xml
The plugin system in Pentaho has been expanded to read perspective definitions from the plugin.xml of a plugin in pentaho-solutions. Any number of perspectives can be added to a single plugin.xml.
<plugin title="My Plugin" name="my-plugin"> <perspective id="myperspective1" title="Perspective 1" layout-priority="1"> </perspective> <perspective id="myperspective2" title="Perspective 2" layout-priority="2"> </perspective> ...and so on </plugin>
If you want to localize the title of the perspective as it appears in PUC you'll need a resource bundle accessible to PUC at runtime. The URI is specified on the perspective definition:
<perspective id="myperspective1" title="${title}" layout-priority="1" resourcebundle="content/default-plugin-perspective/resources/messages/messages"> </perspective>
The plugin "default-plugin-perspective" is a folder in pentaho-solutions/system and contains a messages.properties file located in 'default-plugin-perspective/resources/messages'. This can be made available by publishing a static-path in the plugin config:
<static-paths> <static-path url="/default-plugin-perspective/resources" localFolder="resources"/> </static-paths>
The string $
is replaced with whatever title means in the messages.properties file (or other localized
bundles).
As mentioned before, a perspective will takeover the content area of PUC when it is active. The URL for this is specified with the content-url attribute of the perspective. For example:
<perspective .... content-url="content/default-plugin-perspective/resources/html/index.html">
When this perspective is made active, index.html is loaded in an iframe in the content area of PUC.
Action based security may be used to lock perspectives down, for example, only show an "Admin" perspective to those who are allowed to see it. This is done using the existing action based security provided by the Pentaho platform. The security action for administration is "org.pentaho.security.administerSecurity". To specify this in the perspective definition:
<perspective .... required-security-action="org.pentaho.security.administerSecurity">
In PUC, the order that the perspectives show up in the UI is controlled by a layout-priority attribute in the perspective node. The default perspective (BI Browser) has a value of -1. If you want your perspective to appear before this go with a lower number (such as -2).
One of the most fundamental changes to PUC for the addition of perspectives was to completely replace the menu system with a XUL approach. We already had a XUL definition for the main toolbar, now there is a XUL definition for the main menubar. This file is webapps/pentaho/mantle/xul/main_menubar.xul. You can further customize PUC by changing this XUL file. What we are interested for the purpose of perspectives is the ability to modify the toolbar and menubar with XUL overlays. A perspective can provide any number of XUL overlays which are used to add/remove/update items to the toolbar or menubar. Here is a simple example:
<perspective> <overlay id="myoverlay" resourcebundle="content/default-plugin-perspective/resources/messages/messages"> <toolbar id="mainToolbar"> <toolbarbutton id="mybuttonid" image="../content/default-plugin-perspective/resources/images/enabled32.png" onclick="mainToolbarHandler.executeMantleFunc('defaultTestPerspectiveFunction();')" tooltiptext="${mybutton}" insertafter="dummyPluginContentButton"/> </toolbar> </overlay> </perspective>
This XUL overlay will add a button to the main toolbar with a given image, title, tooltip, insertion point, etc. Menu items can be added anywhere in the menu system in a similar way:
<perspective> <overlay id="myoverlay" resourcebundle="content/default-plugin-perspective/resources/messages/messages"> <menubar id="mainMenubar"> <menubar id="newmenu"> <menuitem id="mymenuitemid" label="${mymenuitemlabel}" command="mainMenubarHandler.executeMantleFunc('defaultTestPerspectiveFunction();')" /> </menubar> </menubar> </overlay> </perspective>
This will add a menuitem under the File -> New menu. You can actually add an entirely new menu if needed. This can be done by giving a more complete definition in the XUL:
<menubar id="mainMenubar"> <menubar id="mymenu" label="${mymenu}" layout="vertical" insertafter="toolsmenu"> <menuitem id="mymenuitem" label="mymenuitem" js-command="alert('Item Clicked')" /> </menubar> </menubar>
When a perspective is active, its overlays will be in play, likewise, when it becomes inactive, its overlays are removed. It is possible for a perspective's overlays to make permanent changes to the UI, instead of just when it is active. This is done simply by convention, an overlay whose "id" starts with "sticky" or "startup" will be active even when the perspective is not. Typically, you would create two overlays for a perspective, one for changes to apply when the perspective is active and another to apply because the plugin exists.
For example:
<overlay id="sticky.myoverlay" resourcebundle="content/default-plugin-perspective/resources/messages/messages"> ...
This overlay will be applied regardless of the active state of the perspective.
Interactivity
It is now possible to more easily interact with the main menubar and toolbar with JavaScript. If your content-url points to an HTML page you can use JavaScript to enable/disable any menu item or toolbar button
To read the state (enabled/disabled) of a toolbar button:
var enabled = window.top.mantle_isToolbarButtonEnabled("button.id");
To set the state of a toolbar button:
var enabled = true; // or false
window.top.mantle_setToolbarButtonEnabled("button.id", enabled);
To read the state of a menu item:
var enabled = window.top.mantle_isMenuItemEnabled('menuitem.id'))
To set the state of a menu item:
window.top.mantle_setMenuItemEnabled('menuitem.id', enabled)
We have also made it easier to bridge between the XUL/GWT world for defining what happens when you press a button or select a menu item. Of course, there are the existing techniques, referencing the handler and a bound function to invoke a well known PUC command, for example:
<toolbarbutton .. onclick="mainToolbarHandler.executeMantleCommand('SaveCommand')" /> <menuitem .. command="mainMenubarHandler.executeMantleCommand('OpenFileCommand')" />
If you want to invoke arbitrary JavaScript with a toolbarbutton or menuitem, you can alternatively define a
js-command attribute in the XUL definition. For example:
<menuitem .. js-command="alert('Hello Perspective')" />
When the menu item is pressed the JavaScript alert function will pop a message saying "Hello Perspective". In this way, we can invoke JavaScript functions defined by the page living at the content-url. Since we are crossing the boundaries of iframes and we don't necessarily know where to call the function, we always eval the JavaScript as it is provided to us. What this means is that you should define any functions you want visible to the toolbar buttons or menu items at the "top" window. An example of this:
<script type="text/javascript"> window.top.myPerspectiveFunction = function() { alert('Success!'); } </script>
To invoke this function from a toolbar button or menu item:
<menuitem .. js-command="myPerspectiveFunction()" />
Update: 11/1/2011
We have added activation/deactivation hooks, if the document (content-url) contains javascript functions "perspectiveActivated" and "perspectiveDeactivated" then we the perspective system will invoke them at the appropriate time. This can be used for any purpose you see fit, practically however, this was created to allow the persistence of perspective state that might otherwise be lost. For example, when a perspective is activated (or reactivated) we can restore the toolbar state (buttons enbabled/disabled). Without the behavior, the XUL overlays are reapplied from the beginning without knowledge of any application state. An example:
<script type="text/javascript"> var testButtonEnabled = true; perspectiveActivated = function() { window.top.mantle_setToolbarButtonEnabled('some.button.id', testButtonEnabled); } perspectiveDeactivated = function() { testButtonEnabled = window.top.mantle_isToolbarButtonEnabled('some.button.id'); } </script>
This example has been checked in along with the existing sample plugin perspective.
Additional JavaScript interaction has been added as well. You can get a list of perspectives (array of perspective ids) and activate a perspective by id. Example use:
window.top.mantle_getPerspectives() and window.top.mantle_setPerspective(id)