Skip to content

SolarNode Webapp Customizing

Matt Magoffin edited this page Nov 22, 2022 · 2 revisions

Custom SolarNode Settings UI

This guide describes how you can extend the built-in SolarNode web settings UI by adding custom resources such as HTML and JavaScript.

Providing a custom setting

The settings UI shows a list of configurable plug-ins:

Component list UI

The SettingSpecifierProvider API is how SolarNode plug-ins expose properties to the settings UI:

Component settings UI

This API allows plug-ins to return any number of SettingSpecifier objects, each of which represents a property to the UI. Typically these are simple, mutable properties such as the TextFieldSettingSpecifier used for mutable string properties. The SolarNode web UI renders HTML form elements on behalf of the plug-in, and manages persisting changes to them via a SettingsService provided by the platform.

As a plug-in developer, you might like to provide a more complex UI than the built-in one can provide. Let's imagine you want to show a custom button that executes some custom JavaScript when pressed:

Custom button UI

To provide a custom UI for a plug-in, a SetupResourceSettingSpecifier can be provided:

public interface SetupResourceSettingSpecifier extends SettingSpecifier {

	/**
	 * Get the provider of setup resources for this specifier.
	 *
	 * @return The resource provider.
	 */
	SetupResourceProvider getSetupResourceProvider();

	/**
	 * Get a set of properties to associate with the resources managed by this
	 * setting.
	 *
	 * @return A set of properties.
	 */
	Map<String, ?> getSetupResourceProperties();

}

This type of setting provides a SetupResourceProvider, which the SolarNode GUI will use to request the plug-in to provide custom resources to inject directly into the rendered HTML for that component:

/**
 * Get a set of resources for specific context and content type.
 *
 * A {@code consumerType} represents the type of application the consumer of
 * the setup resources represents. The {@link #WEB_CONSUMER_TYPE} represents
 * a webapp, for example, and would be interested in resources such as
 * JavaScript, CSS, images, etc.
 *
 * @param consumerType
 *        The consumer type to get all appropriate resources for.
 * @param locale
 *        The desired locale.
 * @return All matching resources, never <em>null</em>.
 */
Collection<SetupResource> getSetupResourcesForConsumer(String consumerType, Locale locale);

The consumerType in this case will be web, to signal to provider that resources appropriate for a webapp are needed. Notice that a java.util.Locale object is passed in as well, so the provider can return localized resources:

Japanese custom button

Here's how a typical plug-in would provide the SetupResourceSettingSpecifier:

// this is configured via Blueprint XML
private SetupResourceProvider customSettingResourceProvider;

public List<SettingSpecifier> getSettingSpecifiers() {
	List<SettingSpecifier> results = new ArrayList<SettingSpecifier>();

	results.add(new BasicSetupResourceSettingSpecifier(
			customSettingResourceProvider,
			Collections.singletonMap("foo", "bar")));

	return results;
}

A BasicSetupResourceSettingSpecifier is returned, which refers to a customSettingResourceProvider that is configured in the plug-in's OSGi Blueprint XML:

<bean id="customSettingResourceProvider"
	class="net.solarnetwork.node.setup.PatternMatchingSetupResourceProvider">
	<property name="basenames">
		<list>
			<value>META-INF/settings/playpen-setting</value>
		</list>
	</property>
</bean>

In this case, the PatternMatchingSetupResourceProvider class is used, which uses a glob-like search path to find resources to expose. The files follow the same naming conventions as used by java.util.ResourceBundle. In this case, files matching META-INF/settings/playpen-setting will be provided by the plug-in:

Eclipse project settings

The above screenshot shows a single localized resource playpen-setting that matches that path:

  • playpen-setting_en_NZ.html
  • playpen-setting_ja.html
  • playpen-setting.html

The most appropriate resource for the requested locale will then be used. This resource contains the HTML seen in the screen shots above:

<button type="button" class="btn btn-info playpen-setting-custom-button">
	Press Me
</button>
<br />
<div class="alert alert-info">
	This is a custom UI from the Setttings Playpen plugin.
</div>

Custom properties

In the example Java code earlier where the BasicSetupResourceSettingSpecifier was configured, you may have noticed a Map was also provided with a foo key associated to a bar value:

	results.add(new BasicSetupResourceSettingSpecifier(
			customSettingResourceProvider,
			Collections.singletonMap("foo", "bar")));

Those properties will be rendered in a wrapper <div> HTML element, as data- attributes like this:

<div data-foo="bar">
	<!-- Settings resource rendered here -->
</div>

This provides a way for the setting to pass runtime property values to the UI, for example to some custom JavaScript behavior as discussed in the next section.

Providing custom behaviors

You may have noticed that the HTML in the previous example did not contain any JavaScript to actually do anything when the button is pressed! Yet if you press the button, an alert appears like this:

Custom UI button alert

The setting might have included JavaScript directly in the HTML resource via a <script> tag, but that is often inefficient. Instead, the plug-in can expose custom JavaScript and CSS resources that get included in the overall page HTML.

Let's start with what we want to achieve: a JavaScript file playpen.js should be included as a linked <script> in the <head> area of the page HTML:

$(document).ready(function() {
	'use strict';

	function setupPlaypen(container) {
		container.find('button.playpen-setting-custom-button').on('click', function(event) {
			var btn = $(event.target),
				foo = btn.parent().data('foo');
			alert("Why, hello there! foo is " +foo +"!");
		});
	}

	$('body').on('sn.settings.component.loaded', function(event, container) {
		setupPlaypen(container);
	});

	setupPlaypen($());
});

This JavaScript uses jQuery to find any <button class="playpen-setting-custom-button"> and adds a click handler that shows a simple alert when the button is pressed that includes the value of the foo data property provided by the button's parent wrapper element.

Note the use of the custom sn.settings.component.loaded event to re-apply the custom initialization. This is needed because the SolarNode UI can dynamically load individual components, after the main UI has loaded. This event gives you visibility into those dynamically loaded components so you can integrate custom behaviors into the newly loaded component, like the click event handler shown in this example.

Our old friend SetupResourceProvider crosses our path again: any SetupResourceProvider registered as an OSGi service will be asked for web resources when the settings HTML is rendered, and application/javascript and text/css resources will be included in the HTML's <head> element, for example:

<script type="application/javascript" src="/a/rsrc/playpen.js"></script>

The path pattern /a/rsrc/{resourceUID} is used to request localized resources from SetupResourceProvider based on their UID:

/**
 * Get a specific resource for a resource UID.
 *
 * @param resourceUID
 *        The ID of the resource to get.
 * @param locale
 *        The desired locale.
 * @return The resource, or {@code null} if not available.
 */
SetupResource getSetupResource(String resourceUID, Locale locale);

In this example, the plug-in registers a provider via some more Blueprint XML:

<service interface="net.solarnetwork.node.setup.SetupResourceProvider">
  <bean class="net.solarnetwork.node.setup.SimpleSetupResourceProvider">
    <property name="resources">
      <list>
        <bean class="net.solarnetwork.node.setup.ClasspathSetupResource">
          <argument value="playpen.js"/><!-- the resourceUID, e.g. URL like /a/rsrc/playpen.js -->
          <argument value="playpen.js"/><!-- the resource filename -->
          <argument value="net.solarnetwork.node.settings.playpen.SettingsPlaypen"/>
          <argument value="#{T(net.solarnetwork.node.setup.SetupResource).JAVASCRIPT_CONTENT_TYPE}"/>
          <argument value="#{T(net.solarnetwork.node.setup.SetupResource).WEB_CONSUMER_TYPES}"/>
          <argument value="#{T(net.solarnetwork.node.setup.SetupResource).USER_ROLES}"/>
        </bean>
      </list>
    </property>
  </bean>
</service>

This time a SimpleSetupResourceProvider is registered, that exposes a single ClasspathSetupResource for the custom JavaScript file we want to inject. The SolarNode settings GUI will inject a link to that resource in the page HTML, making our custom JavaScript behavior available when viewing the setting page.

Clone this wiki locally