Monday, February 24, 2014

Integrating Jenkins, SonarQube, and Nexus

In this post I will describe a solution to add a portal page, a common header and a dynamic panel to each of the three tools Jenkins, SonarQube, and Nexus. Those modifications lead to a better visual integration and intensifies the impression that the tools belong together.

As a pre-requisite you need an Apache HTTP server, configured as reverse proxy and the tools accessible via the following context path:
  • /jenkins
  • /sonarqube
  • /nexus
The folder structure which contains the portal page and the customization fragments looks like this:
  • index.html
  • js
    • portal-panel.js
    • jenkins-customization.js
    • sonarqube-customization.js
    • nexus-customization.js
    • require-2.1.11.js
    • jquery-1.11.0.js

Portal Page

The portal page index.html is available under the context root and basically contains the following links:
  • <a href="https://draft.blogger.com/jenkins">Jenkins</a>
  • <a href="https://draft.blogger.com/sonarqube">SonarQube</a>
  • <a href="https://draft.blogger.com/nexus">Nexus</a>
  • <a href="https://draft.blogger.com/wiki">Wiki</a>
  • <a href="mailto:xxx@yyy.zzz">Mail</a>

Portal Panel

The portal panel is a small box at the top of the page at a fixed position. When the mouse enters the box it gets maximized and displays buttons for accessing the other tools. When the mouse exits the box, the box shrinks to its initial size. When scrolling the page down, the box remains at the top of the window so that the other the tools are accessible from everywhere in the page.



The panel heavily relies on jQuery to do the animations and the styling. The following block contains the script with some information removed for clarity:

function poller()
{
    // wait until jQuery becomes ready
    if(typeof jQuery !== "undefined") {

        // settings
        var animMillis = 200;
        var collapsedWidth = 30;
        var height = 30;
        var expandedWidth = 350;
        var logo = "/img/Logo.png";
        var email = "xxx@yyy.zzz";
        var germanMailText = "Hallo Support Team,%0A%0AQuelle: " +
            window.location.href + "%0A%0A%0A%0AMit freundlichen Gruessen,%0A";
        var englishMailText = "Hello Support Team,%0A%0AOrigin: " +
            window.location.href + "%0A%0A%0A%0AWith Kind regards,%0A";

        // add center method to jQuery
        jQuery.fn.center = function () {
            this.css("position", "fixed");
            this.css("top", "0px");
            var width = jQuery(this).width();
            var left = Math.max(0, ((jQuery(window).width() - width) / 2) + 
                                   jQuery(window).scrollLeft());
            this.css("left", left + "px");
            return this;
        }
        
        // detect mail language
        var language = window.navigator.userLanguage ||
                       window.navigator.language;
        var mailText = englishMailText;
        if (language.indexOf('de') == 0) {
            mailText = germanMailText;
        }

        // add markup (removed for clarity)
        jQuery("body").append('MARKUP');

        // style markup (removed for clarity)
        jQuery("#portalPanel").css("STYLING");
        jQuery("#portalLogo").css("STYLING");
        jQuery("#portalMenu").css("STYLING");
        jQuery(".portalButton").css("STYLING");
        jQuery(".portalButton a:link").css("STYLING");
        jQuery(".portalButtonMagenta").css("STYLING");
        jQuery(".portalButtonDark").css("STYLING");
        jQuery(".portalButtonLight").css("STYLING");

        // button animation
        jQuery(".portalButtonMagenta").hover(function() {
            jQuery(this).css("background-color", "#f59fcd");
        }, function() {
            jQuery(this).css("background-color", "#d8006f");
        });
        jQuery(".portalButtonDark").hover(function() {
            jQuery(this).css("background-color", "#b1b1b1");
        }, function() {
            jQuery(this).css("background-color", "#4c4c4c");
        });
        jQuery(".portalButtonLight").hover(function() {
            jQuery(this).css("background-color", "#f2f2f2");
        }, function() {
            jQuery(this).css("background-color", "#b8b8b8");
        });

        // make box relocate when browser window is resized
        jQuery(window).bind('resize', function() {
            jQuery('#portalPanel').center();
        });

        // initial state
        jQuery("#portalPanel").center();
        jQuery("#portalLogo").show();
        jQuery("#portalMenu").hide();
        
        // animate the box
        var dirin = true;
        jQuery("#portalPanel").hover(function() {
            dirin = true;
            jQuery('#portalMenu').clearQueue().stop(true, true, true);
            jQuery('#portalPanel').clearQueue().stop(true, true, true);
            jQuery('#portalLogo').clearQueue().stop(true, true, true);

            if (dirin) {
                jQuery('#portalLogo').fadeOut(animMillis, function() {
                    if (dirin) {
                        var left = Math.max(0,
                            ((jQuery(window).width() - expandedWidth) / 2) + 
                            jQuery(window).scrollLeft());
                        jQuery('#portalPanel').animate({'width':expandedWidth +
                               'px', 'left':left + 'px'}, animMillis, 'swing',
                            function() {
                              if (dirin) {
                                jQuery('#portalMenu').fadeIn(animMillis,
                                  function() {
                                    // do nothing
                                });
                              }
                        });
                    }
                });
            }
        }, function() {
            dirin = false;
            jQuery('#portalMenu').clearQueue().stop(true, true, true);
            jQuery('#portalPanel').clearQueue().stop(true, true, true);
            jQuery('#portalLogo').clearQueue().stop(true, true, true);
            
            if (!dirin) {
                jQuery('#portalMenu').fadeOut(animMillis, function() {
                    if (!dirin) {
                        var left = Math.max(0,
                            ((jQuery(window).width() - collapsedWidth) / 2) +
                            jQuery(window).scrollLeft());
                        jQuery('#portalPanel').animate({'width':collapsedWidth +
                               'px', 'left':left + 'px'}, animMillis, 'swing',
                            function() {
                              if (!dirin) {
                                jQuery('#portalLogo').fadeIn(animMillis,
                                  function() {
                                    jQuery('#portalLogo').show();
                                });
                            }
                        });
                    }
                });
            }
        });
    }
    else
    {
        setTimeout(poller, 100);
    }    
}

poller();

Jenkins Customization

To use jQuery in our customization fragments the Jenkins jQuery plugin must be installed via the Jenkins Update Center. The plugin adds the jQuery JavaScript library to each Jenkins page without colliding with Jenkins' JavaScript. Because Jenkins uses the jQuery shortcut "$" for one of its own functions, jQuery can only be accessed by using the full name "jQuery".
Download the latest release of the page-markup plugin and copy the page-markup.hpi to the folder JENKINS_HOME/plugins
Finally the plugins need to be activated by restarting the Jenkins server.

Add the following snippet to the text area under Manage Jenkins -> Configure System -> Additional Page HTML Markup -> Header HTML to extend the Jenkins HTML page:

<style type="text/css">
#top-panel {
  display:none;
  background: none;
}
.top-sticker, #top-sticker {
  top: 62px;
}
</style>
<script src="/js/require-2.1.11.js"></script>
<script>
require(["/js/jenkins-customization.js"], function(util) {
  // nothing to do
});
require(["/js/portal-panel.js"], function(util) {
  // nothing to do
});
</script>

The small CSS part hides the Jenkins logo and the blue header so that it does not flicker when the page is reloaded. Because the jQuery code takes some time to get executed, the logo is displayed shortly if no countermeasures are taken.

SonarQube Customization

Download the sonar-pagedecoration-plugin and install it manually to the $SONARQUBE_HOME/extensions/plugins folder of your SonarQube installation. You need to restart SonarQube to get the plugin activated.

Add the following snippet to the Settings -> General Settings -> Page Decoration -> Script text area and click on Save Page Decoration Settings to add the fragment to the SonarQube HTML page:

<script src="/js/require-2.1.11.js"></script>
<script>
require(["/js/sonarqube-customization.js"], function(util) {
  // nothing to do
});
require(["/js/portal-panel.js"], function(util) {
  // nothing to do
});
</script>

The SonarQube customization does not need to load jQuery explicitly as SonarQube already uses jQuery in its HTML pages.

Nexus Customization

Download the nexus-pagedecoration-plugin, unzip the file nexus-pagedecoration-plugin-2.7.1-01-bundle.zip to the folder SONATYPE_WORK/nexus/plugin-repository and restart Nexus to activate the plugin.  

Add the following snippet to the Administration -> Page Decoration  -> Pre Head text area and click on Save to add the fragment to the Nexus HTML page:

<style type="text/css">
load.
div#branding img {
  display:none;
}
div#header {
  background-color: white;
  background-image: none;
}
</style>
<script src="/js/require-2.1.11.js"></script>
<script>
require(["/js/jquery-1.11.0.min.js"], function(util) {
  require(["/js/nexus-customization.js"], function(util) {
    // nothing to do
  });
  require(["/js/portal-panel.js"], function(util) {
    // nothing to do
  });
});
</script>

In Nexus the original logo also needs to be hidden to avoid flickering upon page reload.
The Nexus UI is based upon ExtJS so that jQuery needs to pulled first. When jQuery has been loaded completely, the other JavaScript files get loaded and are executed.

The scripts can be found in the following GitHub project:

No comments:

Post a Comment