How to write a Jenkins Plugin – Part 2

In this part we will create a form fragment for the global config page using Jelly, and see how to save and use changes to the config submitted by users. Then we’ll customise the view tabs Jelly to call our plugin.

Global Config

OK, we want some configuration options to apply settings to this plugin, as opposed to per-build options which are done slightly differently.

Jenkins uses an XML scripting engine called Jelly (see Apache Jelly). It’s a lot like JSP or Velocity, and later you’ll see how to create custom Jelly tag libraries to extend the Jenkins UI.

For our global config we need a global.jelly resource file in a package having the same name as our plugin. Our plugin class is called CustomViewsTabBar and it lives in org.jenkinsci.plugins.customviewtabs under src/main/java, so our global.jelly goes in org.jenkinsci.plugins.customviewtabs.CustomViewsTabBar under src/main/resources.

Hunt through the Jenkins developer guides, look at the source of some other plugins, and even browse through the contents of the lib.layout package inside the jenkins-core jar in your project’s maven dependencies, and you’ll see lots of examples of using Jelly.

Here we’re just going to add a simple textbox to get some label text for the tab, and an “advanced” options where we’re going to have a drop-down selection list to choose a tab colour. There’s no real purpose for these settings other than to give a simple example that illustrates the principle and helps us get started with plugin development.


<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler"
 xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson"  xmlns:f="/lib/form">

<f:section title="Custom View Tabs">

  <f:entry field="labelText" title="Text for tab label">
    <f:textbox />
  </f:entry>

  <f:advanced title="Advanced Settings">
    <f:entry field="tabColour" title="Tab colour">
      <f:select />
    </f:entry>
  </f:advanced>

</f:section> 
</j:jelly>

That XML ought to be pretty self-explanatory, but go ahead and fire up the plugin with the usual command

mvn hpi:run -Djetty.port=8090 -Pjenkins

then hit the global config page at http://localhost:8090/jenkins/configure and we can see it in action.

Configure System  Jenkins

We have a text box, and a button we can press to expand advanced options.

If you did run the plugin you will have noticed an exception from Jenkins:

java.lang.IllegalStateException: class org.jenkinsci.plugins.customviewtabs.CustomViewsTabBar$CustomViewsTabBarDescriptor doesn't have the doFillTabColourItems method for filling a drop-down list

This should remind you what we learned about descriptors earlier. Jenkins expects to be able to exchange the configuration settings between the global.jelly form and our plugin’s descriptor. When it wants to show the form, it asks the descriptor for the current values, and when a user changes and saves the config, Jenkins will pass the new settings back to our descriptor.

So for this to work properly, we’re going to need getters and setters matching the configuration items implied by the entries in our global.jelly config.

For Jenkins to get the current value for our ‘labelText’ text box, we’re going to need String getLabelText(), but clearly our ‘tabColour’ selection dropdown is expecting something a little different. When Jenkins wants to display the list of available options it will need to know the names and values of all the available options, and of course, which one is currently selected.

The missing doFillTabColourItems method will return the list of options as a hudson.util.ListBoxModel containing a list of hudson.util.ListBoxModel.Option, something like this (simplistically):

public ListBoxModel doFillTabColourItems(){

    return new ListBoxModel(
        new Option("Green", "00ff00", true),
        new Option("Yellow", "ffff00", false),
        new Option("Red", "ff0000", false));
}

How did I know that? By hunting through the Jenkins documentation and browsing some other plugins. There isn’t really a very good one-stop shop for this kind of information, so the best starting point is to find a plugin that has features you’re interested in, and take a look inside.

So let’s add that missing method, and lets also add a field and a getter for the label text. This should get our global config populated with initial values and then we can look at saving. Add to the descriptor:

private String labelText = "initial";

public String getLabelText(){
    return labelText;
}

and re-deploy the app. Hit the config page, press the button to open our ‘advanced’ settings and we should see the correct set of options and initial values.

global config initial values

Good. Try entering new values and saving them though, and nothing happens. Not good.

We need to implement our descriptor’s ‘configure’ method. When the user saves the form, Jenkins will send us the form data as JSON, and we need to extract the values and save them.

Save them? Where will it get saved? Well, remember in part one we mentioned the save() and load() methods that our descriptor would need to call, and that Jenkins would automatically manage saving and loading our data to and from an XML file.

Now that we fixed the previous exception, if you did run the plugin in the last step then you should now be able to find that XML file for our plugin’s persistent data. If you used the hpi:run command in your maven project directory then you should now have a ‘work’ directory containing all the output from the Jetty server, and in there you will see an XML file named for your plugin, eg org.jenkinsci.plugins.customviewtabs.CustomViewsTabBar.xml

Take a look and you should see a single entry for the default value of our one property:

<labelText>initial</labelText>

So now lets process that JSON data and perist the user’s new selections. For the labelText it’s really simple:

@Override
public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {

    labelText = formData.getString("labelText");

    save();
    return false;
}

What about the dropdown? Well actually that’s really simple too, because Jenkins only has to pass us one value, the value for the selected option. Earlier when we had new Option("Yellow", "ffff00", false), that created a dropdown option, set to selected=false, with the display name ‘Yellow’ and the value ‘ffff00’, which is what will be returned to us if Yellow was selected.

So we just need:

@Override
public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {

    labelText = formData.getString("labelText");
    tabColour = formData.getString("tabColour");

    save();
    return false;
}

and of course a suitable field to assign the selected tabColour value. You know how simple it is to work with JSON and you can see how simple it is to exchange these config values between the plugin and the UI.

One thing missing though. As it is, if you were to save the new values, although the plugin will remember the new value for the tabColour field, when we dish up the list of Options for the dropdown it will not reflect the currently selected value because we were setting Green as the selected option.

So, modify your doFillTabColourItems method to make use of the current setting, then fire it all up again, change the values and hit save. The new settings are remembered this time.

Here’s a hideous way to do that:


public ListBoxModel doFillTabColourItems(){

  return new ListBoxModel(
    new Option("Green", "00ff00", tabColour.equals("00ff00")),
    new Option("Yellow", "ffff00", tabColour.equals("ffff00")),
    new Option("Red", "ff0000", tabColour.equals("ff0000")));
}

Happy happy joy joy. Take a look inside your persistent XML file again. It should now have two entries, and it should reflect whatever you just selected in the global config page:


<labelText>My New Value</labelText>
<tabColour>ff0000</tabColour>

This is all really easy, right?

Customising the Jenkins UI with Jelly

Good. Now let’s try to use our settings from the Jenkins UI. We’re going to change the label on the view tab. Why? Well, just to give an example of how to do it. I needed to do it in my plugin because I wanted to show a custom message about the number of failed jobs in the view. For example, given a view called “Project One”, with 2 failed jobs, and a custom label text like “Failures:” the plugin could dynamically change the tab label to be “Project One – Failures:2”.

Doesn’t really matter if that would be useful or not, lets just use it as an example to see how it works.

We’ll come back to figuring out how many failed jobs there are. For now lets try to change the tab label by adding our custom label text after the original name.

The view tabs UI comes from a jelly file, viewTabs.jelly in hudson.views.DefaultViewsTabBar in jenkins-core.jar. We’ll start with a copy of that, which we need to put in our resource directory alongside our global.jelly.

It starts like this:


<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<!-- view tab bar -->
<l:tabBar>
<j:forEach var="v" items="${views}">
<l:tab name="${v.viewName}" active="${v==currentView}" href="${rootURL}/${v.url}" />
</j:forEach>
<j:if test="${currentView.hasPermission(currentView.CREATE)}">
<l:tab name="+" href="${rootURL}/${currentView.owner.url}newView" active="false"
title="${%New View}" />
</j:if>
</l:tabBar>
</j:jelly>

You can see it iterates through the views, and for each view it makes a tab using the view’s ‘viewName’ as the label.

If you read about the way Jenkins binds Jelly fragments to Java objects, you’ll eventually discover that you can call the methods of your plugin from jelly by using the ‘it’ reference, which refers to your plugin.

First, lets add a method on our plugin that we can call from jelly:

public String getTabLabel(){    	
    	 CustomViewsTabBarDescriptor descriptor = (CustomViewsTabBarDescriptor) super.getDescriptor();    	
    	 return descriptor.getLabelText();
    }

And now lets call that method in the viewtabs.jelly as it.getTabLabel(), like so:


<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<!-- view tab bar -->
<l:tabBar>
<j:forEach var="v" items="${views}">
<l:tab name="${v.viewName} : ${it.getTabLabel()}" active="${v==currentView}" href="${rootURL}/${v.url}" />
</j:forEach>
<j:if test="${currentView.hasPermission(currentView.CREATE)}">
<l:tab name="+" href="${rootURL}/${currentView.owner.url}newView" active="false"
title="${%New View}" />
</j:if>
</l:tabBar>
</j:jelly>

Fire up Jenkins as before. You’ll need to add some jobs before you get to see the view tabs, so create a new job as a Maven job, just giving it a name and saving it. This will fail if you run it. Create another job as a freestyle project, again just giving it a name. This one should succeed if you run it. These will be handy later when we want to count the jobs in view and their statuses.

Now you should see that the “All” view tab includes our custom text in the label. If you don’t see it, first check whether you told Jenkins to use the custom views tab bar as the views tab bar in the global config page.

Modified  Tab label

You can play around with the Jelly and any changes will be automatically deployed through the running Jetty instance and visible when you refresh the web page.

Here’s where things start to get a bit more interesting. How do we change the tab colour to use the value set via our plugin’s global config?

In viewTabs.jelly we create the tab instances, and they have attributes for ‘name’, ‘active’ and ‘href’. They are defined as ‘l:tab’ and that namespace takes us to xmlns:l="/lib/layout". If we look in the lib.layout package in jenkins-core.jar, we’ll find the corresponding tab.jelly file.

Let’s take a copy of that to be the starting point for our custom tag library. We’ll save it into a file called custom-tab.jelly in src/main/resources/lib/customviewtabs.

In that same package we’re also going to need an empty file called taglib, which is a marker to tell Jenkins it will find a jelly tag library here.

In our custom-tab, we’ll make it have a new attribute for colour:

<%@ attribute name="colour" required="true" type="java.lang.String" %>

and we’ll use the colour to set the background-colour of the td element. This appears twice; Once for active and once for inactive, eg:

td class="active" style="background-color:#${colour}"

Now we just need to use this cutom-tab in our viewTabs.jelly.

Add a new namespace such as xmlns:cvt="/lib/customviewtabs", change l:tab to cvt:custom-tab, and pass in the colour attribute by calling a method on our plugin:

colour="${it.getTabColour()}"

Of course, you’ll need to add that method.

Re-deploy, and your tabs should now have colour. Try changing the settings for tab colour and see if it is propagated.

Success.

coloured tab

Of course, we haven’t worked out how to display the number of failed jobs in view, or to automatically select a tab colour that reflects the worst status of jobs in the view, but we have got a plugin that can be configured by users, persists the configuration, and modifies built in UI components with a custom Jelly tag library.

In the next part we’ll find a way of displaying the number of failed jobs on the tab label, and then we’ll go through the process of getting your Jenkins plugin hosted and released.

Advertisements
Posted in Jenkins, Jenkins Plugins
10 comments on “How to write a Jenkins Plugin – Part 2
  1. Stefsytem says:

    At the following lines I got an error message. I don’t know how to solve this problem.
    ——————————————–
    public String getTabLabel(){
    CustomViewsTabBarDescriptor descriptor = (CustomViewsTabBarDescriptor) super.getDescriptor();
    return descriptor.getLabelText();
    }
    ——————————————–
    Error: The method getDescriptor() is undefined for the type ViewsTabBarDescriptor

    • Todderz says:

      Hi,

      getDescriptor is a method on CustomViewsTabBar (actually it’s in ViewsTabBar so it will be visible in whatever class you have that is extending ViewsTabBar).

      Your error message says it’s trying to call the method where “super” is ViewsTabBarDescriptor, so it seems you have that method call somewhere inside ViewsTabBarDescriptor.

      You need to re-position that method or provide a suitable intermediary. Hope that helps.

      Apologies if this is an error in my notes. When I get chance I’ll check through and make corrections.

      • jrz says:

        Hi,

        I am getting the same error message, at the same place in the tutorial. Have you been able to come up with a fix or workaround?

      • Todderz says:

        Hi,

        You need to have the getTabLabel method inside the CustomViewsTabBar class, not inside the nested CustomViewsTabBarDescriptor class

  2. Hi Todderz,

    I appreciate your tutorial! Regarding the plugin I’m creating, my requirements must display checkboxes on the project page, which I am trying to bind to an Action. I’ve looked into the ProminentProjectAction interface, but to no avail.
    Do you know of a sure way to bind data from a floatingBox.jelly to a Java object?

  3. I followed this but things at my end are not working I cant see test appended to view name. No error at my end.Simply it does not work.

    I have created viewTabs.jelly in the same place where global.jelly exists. Added the getTabLabel() method to CustomViewsTabBar class and ran the jenkins. Once it was up i created two jobs and ran them. one failed and one was success. I refreshed my jenkins but no change in tab UI.

  4. hi , where to add this td class=”active” style=”background-color:#${colour}” inside custom-tab.jelly

    Thanks

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: