<html><head><style type='text/css'>p { margin: 0; }</style></head><body><div style='font-family: times new roman,new york,times,serif; font-size: 12pt; color: #000000'>Hi guys,<br><br>the latest revision of UI Plugins proof-of-concept patch is now available for you to experiment with. You can download the patch from oVirt Gerrit at http://gerrit.ovirt.org/#/c/8120/2 (patch set 2).<br><br>Please read on to learn what's new in this revision. If you have any comments, questions or ideas, please let me know!<br><br><hr style="width: 100%; height: 2px;"><br><strong>0. UI plugin path information resolved using local Engine configuration</strong><br style="font-weight: bold;"><br>Server-side UI plugin infrastructure now uses local (machine-specific) Engine configuration instead of global (<em>vdc_options</em> database table) Engine configuration:<br><ul><li>Previously, path information was resolved through org.ovirt.engine.core.common.config.Config
class - Engine configuration values were retrieved from <span style="font-style: italic;">vdc_options</span> database table.</li><li>Currently, path information is resolved through org.ovirt.engine.core.utils.LocalConfig
class - Engine configuration values are retrieved from local
file system.</li></ul>In case you're not working with oVirt Engine through RPM package system, e.g. you have a local development environment set up and you build and deploy oVirt Engine through Maven, please follow these steps:<br><br>a. Copy default Engine configuration into /usr/share/<span style="font-weight: bold;">ovirt-engine</span>/conf<br><br><div style="margin-left: 40px; font-family: courier new,courier,monaco,monospace,sans-serif;"># mkdir -p /usr/share/ovirt-engine/conf<br># cp <OVIRT_HOME>/backend/manager/conf/engine.conf.defaults /usr/share/ovirt-engine/conf/engine.conf.defaults</div><br>b. If necessary, copy UI plugin data files from /usr/share/engine/ui-plugins to /usr/share/<span style="font-weight: bold;">ovirt-engine</span>/ui-plugins<br><br>c. If necessary, copy UI plugin config files from /etc/engine/ui-plugins to /etc/<span style="font-weight: bold;">ovirt-engine</span>/ui-plugins<br><br>d, In case you want to override the default Engine configuration, put your custom property file into /etc/sysconfig/ovirt-engine<br><br>The reason behind this change is that path information for UI plugin data and configuration is typically machine-specific, and should be customizable per machine through Engine local configuration.<br><br><hr style="width: 100%; height: 2px;"><br><span style="font-weight: bold;">1. New plugin API function: addMainTabActionButton</span><br style="font-weight: bold;"><br>The "addMainTabActionButton" API adds custom context-sensitive button to the given main tab's data grid, along with corresponding data grid context menu item.<br><br><span style="font-family: courier new,courier,monaco,monospace,sans-serif;">addMainTabActionButton(entityTypeName, label, actionButtonInterface)</span><br style="font-family: courier new,courier,monaco,monospace,sans-serif;"><em style="font-family: courier new,courier,monaco,monospace,sans-serif;"></em><br><span style="font-style: italic;">entityTypeName</span> indicates which main tab's data grid the button should be added to, according to the entity type associated with the main tab.<span style="font-style: italic;"> entityTypeName</span> values are strings reflecting org.ovirt.engine.ui.webadmin.plugin.entityEntityType enum members. Following <span style="font-style: italic;">entityTypeName</span> values are currently supported (values are case-sensitive): "DataCenter", "Cluster", "Host", "Storage", "Disk", "VirtualMachine", "Template".<br><br>Note: "Pool" value is currently not supported, because of org.ovirt.engine.core.common.businessentities.vm_pools entity not implementing the BusinessEntity interface, not sure why though. Maybe we should switch from BusinessEntity to IVdcQueryable interface and always cast getQueryableId method result value to Guid?<br><br><span style="font-style: italic;">label</span> is the title displayed on the button<span style="font-style: italic;">.<br></span><br><span style="font-style: italic;">actionButtonInterface</span> represents an object that "implements the button interface" by declaring its functions: <span style="font-style: italic;">onClick</span>, <span style="font-style: italic;">isEnabled</span>, <span style="font-style: italic;">isAccessible</span>. All functions of <span style="font-style: italic;">actionButtonInterface</span> receive currently selected item(s) as function arguments.<br><br>Let's take a closer look at the concept behind <span style="font-style: italic;">actionButtonInterface</span>. In traditional class-based object-oriented languages, such as Java, interface is an abstract type that contains method declarations without an implementation. A class that implements the given interface must implement all methods declared by that interface (unless it's an abstract class, but this isn't relevant in our case).<br><br>In contrast with traditional class-based object-oriented languages, JavaScript supports OOP through prototype-based programming model (https://developer.mozilla.org/en-US/docs/JavaScript/Introduction_to_Object-Oriented_JavaScript). At the same time, JavaScript language is dynamically-typed and therefore doesn't support traditional concept of interface in OOP, it uses "duck typing" technique instead (http://en.wikipedia.org/wiki/Duck_typing).<br><br>The simplest way to provide an object that "implements the given interface" in JavaScript is to use "duck typing" technique: providing an object that contains well-known functions. In UI plugin infrastructure, I call this concept "interface object", represented by org.ovirt.engine.ui.webadmin.plugin.jsni.JsInterfaceObject class. Unlike the traditional concept of interface abstract type in object-oriented languages, an "interface object" <u>does not necessarily have to declare all functions of the given interface</u> in order to "implement" such interface. In fact, an empty object can be used as a valid "interface object". Missing functions will be simply treated as empty (no-op) functions. Furthermore, an "interface object" can "implement" multiple interfaces by declaring functions of those interfaces (interface composition).<br><br>Getting back to "addMainTabActionButton" API, here's a sample code that adds new button to "Host" main tab data grid, as part of UiInit event handler function:<br><br><span style="font-family: courier new,courier,monaco,monospace,sans-serif;">UiInit: <span style="font-weight: bold; color: rgb(153, 0, 0);">function</span>() {</span><br style="font-family: courier new,courier,monaco,monospace,sans-serif;"><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"></span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> api.<span style="color: rgb(204, 51, 204);">addMainTabActionButton</span>('Host', 'Single-Host Action',<br><br><span style="color: rgb(0, 102, 0);"> // Action button interface object</span><br style="color: rgb(0, 102, 0);"><span style="color: rgb(0, 102, 0);"> // All functions receive currently selected item(s) as function arguments</span><br style="color: rgb(0, 102, 0);"> {</span><br><br><span style="font-family: courier new,courier,monaco,monospace,sans-serif; color: rgb(0, 102, 0);"> // Called when the user clicks the button</span><br style="color: rgb(0, 102, 0);"><span style="font-family: courier new,courier,monaco,monospace,sans-serif; color: rgb(0, 102, 0);"></span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> <span style="color: rgb(0, 0, 153);">onClick</span>: <span style="font-weight: bold; color: rgb(153, 0, 0);">function</span>() {<br></span><span style="font-family: courier new,courier,monaco,monospace,sans-serif; color: rgb(0, 102, 0);"> // Calling 'arguments[0]' is safe, because onClick() can be called<br> // only when exactly one item is currently selected in the data grid</span><br style="font-family: courier new,courier,monaco,monospace,sans-serif; color: rgb(0, 102, 0);"><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;">window.alert('Selected host entity ID = ' + arguments[0].entityId);</span><br style="font-family: courier new,courier,monaco,monospace,sans-serif;"><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;">},</span><br><br><span style="font-family: courier new,courier,monaco,monospace,sans-serif; color: rgb(0, 102, 0);"> // Returning 'true' means the button is enabled (clickable)</span><br style="color: rgb(0, 102, 0);"><span style="font-family: courier new,courier,monaco,monospace,sans-serif; color: rgb(0, 102, 0);"> // Returning 'false' means the button is disabled (non-clickable)</span><br style="font-family: courier new,courier,monaco,monospace,sans-serif; color: rgb(0, 102, 0);"><span style="font-family: courier new,courier,monaco,monospace,sans-serif; color: rgb(0, 102, 0);"> // Default value = 'true'</span><br style="font-family: courier new,courier,monaco,monospace,sans-serif; color: rgb(0, 102, 0);">
<span style="font-family: courier new,courier,monaco,monospace,sans-serif;"></span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"><span style="color: rgb(0, 0, 153);">isEnabled</span>: <span style="font-weight: bold; color: rgb(153, 0, 0);">function</span>() {<br></span><span style="font-family: courier new,courier,monaco,monospace,sans-serif; color: rgb(0, 102, 0);"> // Enable button only when exactly one item is selected</span><br style="font-family: courier new,courier,monaco,monospace,sans-serif; color: rgb(0, 102, 0);"><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"><span style="font-weight: bold; color: rgb(153, 0, 0);">return</span> arguments.length == 1;</span><br style="font-family: courier new,courier,monaco,monospace,sans-serif;"><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;">},</span><br><br><span style="font-family: courier new,courier,monaco,monospace,sans-serif; color: rgb(0, 102, 0);"> // Returning 'true' means the button is visible<br> // Returning 'false' means the button is hidden</span><br style="font-family: courier new,courier,monaco,monospace,sans-serif; color: rgb(0, 102, 0);"><span style="font-family: courier new,courier,monaco,monospace,sans-serif; color: rgb(0, 102, 0);"> // Default value = 'true'</span><br style="font-family: courier new,courier,monaco,monospace,sans-serif; color: rgb(0, 102, 0);">
<span style="font-family: courier new,courier,monaco,monospace,sans-serif;"></span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"></span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"><span style="color: rgb(0, 0, 153);">isAccessible</span>: <span style="font-weight: bold; color: rgb(153, 0, 0);">function</span>() {</span><br><span style="font-family: courier new,courier,monaco,monospace,sans-serif; color: rgb(0, 102, 0);"> // Always show the button in the corresponding data grid</span><br style="font-family: courier new,courier,monaco,monospace,sans-serif; color: rgb(0, 102, 0);"><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"><span style="font-weight: bold; color: rgb(153, 0, 0);">return</span> <span style="font-weight: bold; color: rgb(153, 0, 0);">true</span>;</span><br style="font-family: courier new,courier,monaco,monospace,sans-serif;"><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;">}</span><br><br style="font-family: courier new,courier,monaco,monospace,sans-serif;"><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;">}<br><br> );</span><br style="font-family: courier new,courier,monaco,monospace,sans-serif;"><span style="font-family: courier new,courier,monaco,monospace,sans-serif;">}</span><br><br>As mentioned above, all functions of an interface object are optional. For functions expecting return value, default value is defined by UI plugin infrastructure. For example:<br><ul><li>onClick - no default value (no return value expected)</li><li>isEnabled / isAccessible - default value "true" (boolean return value expected)</li></ul><p></p><p>Note: UI plugin infrastructure checks the actual return value type, and uses default value in case the function returned something of wrong (unexpected) type.<br></p><br>In the example above, "currently selected item(s)" maps to JSON-like representations of business entities currently selected in the corresponding data grid. For now, the entity representation is quite simple and same for all entity types:<br><br><span style="font-family: courier new,courier,monaco,monospace,sans-serif;">{ entityId: "[BusinessEntityGuidAsString]" }</span><br style="font-family: courier new,courier,monaco,monospace,sans-serif;"><br>In future, we will create specific JSON-like representations for specific business entities, in compliance with Engine REST API entity structure.<br><br>For a more extensive example of using "addMainTabActionButton" API, please see the attached "addMainTabActionButton.html.example" file.<br><br><hr style="width: 100%; height: 2px;"><br><span style="font-weight: bold;">2. Improved plugin API function: addMainTab</span><br style="font-weight: bold;"><br>The "addMainTab" API was improved to address following issues:<br><ul><li>"addMainTab" can now be called at any moment during UI plugin runtime, given that the plugin is allowed invoke plugin API functions (plugin is either INITIALIZING or IN_USE).<br>Previously, "addMainTab" worked reliably only when called from within UiInit event handler function.<br>Currently, it's possible to call "addMainTab" at any moment, e.g. from within some other event handler function (after UiInit has completed).</li></ul><ul><li>"addMainTab" now retains "active" tab (highlighted tab GUI).<br>"addMainTab" works by adding new tab component (GWTP presenter proxy) and refreshing main tab panel GUI by removing all related tabs and re-adding them again.<br>This logic is handled by org.ovirt.engine.ui.common.presenter.DynamicTabContainerPresenter class, which makes sure that "active" tab is retained even after main tab panel was refreshed.<br></li></ul><p></p>Furthermore, custom main tab implementation now displays the content of the given URL through HTML iframe element.<br><br><hr style="width: 100%; height: 2px;"><br><span style="font-weight: bold;">3. Improved native JavaScript function handling</span> (GWT JSNI)<br><br>This patch introduces org.ovirt.engine.ui.webadmin.plugin.jsni.JsFunction and org.ovirt.engine.ui.webadmin.plugin.jsni.JsFunctionResultHelper classes providing Java abstraction for invoking native JavaScript functions. These classes follow the general contract of "interface object" as mentioned above.<br><br>JsFunctionResultHelper is particularly useful when dealing with functions which are expected to return value of a certain type. Too bad standard GWT JSNI classes don't provide such abstraction for working with native functions out-of-the-box...<br><br><hr style="width: 100%; height: 2px;"><br><span style="font-weight: bold;">4. ActionPanel and ActionTable type hierarchy refactoring</span> (related to "addMainTabActionButton" API)<br style="font-weight: bold;"><br>Previously, AbstractActionPanel and AbstractActionTable classes didn't implement any reasonable interface that would allow other components (client-side UI plugin infrastructure) to depend on their functionality in a loosely-coupled manner. This would make code that implements "addMainTabActionButton" API "ugly": main tab view interface would have to reference AbstractActionTable class directly. In MVP design pattern, view interface should avoid referencing specific GWT Widget classes directly.<br><br>This patch introduces new interfaces for ActionPanel and ActionTable components while eliminating code redundancy (duplicate or unnecessary code).<br><br><hr style="width: 100%; height: 2px;"><br><span style="font-weight: bold;">5. ActionPanel type hierarchy refactoring</span> (related to "addMainTab" API)<br><br>Since org.ovirt.engine.ui.common.presenter.DynamicTabContainerPresenter defines new DynamicTabPanel interface that extends standard GWTP TabPanel interface, some refactoring had to be done in related ActionPanel classes.<br><br>This patch makes sure that both org.ovirt.engine.ui.common.widget.tab.AbstractTabPanel (widget) and org.ovirt.engine.ui.common.view.AbstractTabPanelView (view) support DynamicTabPanel interface.<br><br>Note that for now, only main tab panel (org.ovirt.engine.ui.webadmin.section.main.presenter.MainTabPanelPresenter) supports dynamic tabs within its view.<br><br><hr style="width: 100%; height: 2px;"><br><span style="font-weight: bold;">Where is addSubTab API function?</span><br><br>Implementing "addSubTab" API requires some more changes, and I didn't want to delay this PoC patch just because of it...<br><br>Here's a sample code that illustrates proposed "addSubTab" API usage:<br><br><span style="font-family: courier new,courier,monaco,monospace,sans-serif;">UiInit: <span style="font-weight: bold; color: rgb(153, 0, 0);">function</span>() {</span><br style="font-family: courier new,courier,monaco,monospace,sans-serif;">
<span style="font-family: courier new,courier,monaco,monospace,sans-serif;"></span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> api.<span style="color: rgb(204, 51, 204);">addSubTab</span>('Host', <span style="color: rgb(0, 102, 0);">// entityTypeName</span><br> 'Custom Host Sub Tab', <span style="color: rgb(0, 102, 0);">// label</span><br> 'custom-host-sub-tab', <span style="color: rgb(0, 102, 0);">// historyToken</span><br> 'http://www.ovirt.org/', <span style="color: rgb(0, 102, 0);">// contentUrl<br><br></span> <span style="background-color: rgb(255, 255, 255); color: rgb(0, 102, 0);">// Sub tab interface object<br> // </span></span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"><span style="color: rgb(0, 102, 0);">All functions receive currently selected item(s)<br> // within the main tab data grid as function arguments</span></span><br style="color: rgb(0, 102, 0);"><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"><span style="background-color: rgb(255, 255, 255); color: rgb(0, 102, 0);"></span>
{</span><br>
<br>
<span style="font-family: courier new,courier,monaco,monospace,sans-serif; color: rgb(0, 102, 0);"></span><span style="font-family: courier new,courier,monaco,monospace,sans-serif; color: rgb(0, 102, 0);"> // Returning 'true' means the sub tab is visible<br>
// Returning 'false' means the sub tab is hidden</span><br style="font-family: courier new,courier,monaco,monospace,sans-serif; color: rgb(0, 102, 0);">
<span style="font-family: courier new,courier,monaco,monospace,sans-serif; color: rgb(0, 102, 0);"> // Default value = 'true'</span><br style="font-family: courier new,courier,monaco,monospace,sans-serif; color: rgb(0, 102, 0);">
<span style="font-family: courier new,courier,monaco,monospace,sans-serif;"></span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"></span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"><span style="color: rgb(0, 0, 153);">isAccessible</span>: <span style="font-weight: bold; color: rgb(153, 0, 0);">function</span>() {<br></span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"><span style="font-weight: bold; color: rgb(153, 0, 0);">return</span> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;">arguments.length == 1</span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> && </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;">arguments[0].entityId == '<MyHostEntityId>'</span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;">;</span><br style="font-family: courier new,courier,monaco,monospace,sans-serif;">
<span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;">}</span><br>
<br style="font-family: courier new,courier,monaco,monospace,sans-serif;">
<span style="font-family: courier new,courier,monaco,monospace,sans-serif;"> </span><span style="font-family: courier new,courier,monaco,monospace,sans-serif;">}<br>
<br>
);</span><br style="font-family: courier new,courier,monaco,monospace,sans-serif;">
<span style="font-family: courier new,courier,monaco,monospace,sans-serif;">}</span><br><br>As part of "addSubTab" API implementation, I'll refactor custom main tab components, in order to use one "tab type" for both main and sub tabs.<br><br>Currently, we have one (and only one) "tab type" - a tab that shows content of the given URL through HTML iframe element.<br><br>We could also create new "tab types", e.g. form-based tab that shows key/value pairs (IMHO this could be quite useful for custom sub tabs).<br><br><hr style="width: 100%; height: 2px;"><br>Let me know what you think!<br><br>Cheers,<br>Vojtech<br><br></div></body></html>