In creating the Splunk for Web Intelligence App's custom setup workflow, some 'stretch' requirements (nice to have, but not essential) were defined as follows:
To meet these requirements involving dynamic user interaction, it was necessary to create some custom JavaScript.
Per the related How-To on setup.html template, the JavaScript, HTML, and CSS components of the Splunk for Web Intelligence App's setup workflow were placed into separate files. All setup workflow JavaScript was placed in the following location:
./webintelligence/appserver/static/setup.js
To ensure that setup.js is included in a script tag when setup.html is rendered, setup.js was specified in the template's call to load custom script files:
<%def name="custom_css()">
<%lib:stylesheet_tags files="${['/static/app/webintelligence/setup.css']}" />
<%lib:script_tags files="${['/static/app/webintelligence/setup.js']}" />
</%def>
In general, the best practice is for custom JavaScript handlers to be bound to events inside a $jQuery.ready() call. To better understand why, consider this excerpt from the jQuery documentation:
While JavaScript provides the load event for executing code when a page is
rendered, this event does not get triggered until all assets such as images have
been completely received. In most cases, the script can be run as soon as the DOM
hierarchy has been fully constructed. The handler passed to .ready() is guaranteed
to be executed after the DOM is ready, so this is usually the best place to attach
all other event handlers and run other jQuery code.
Thus, to insert our bindHandler() method into the DOM, the following code is used:
$(document).ready(function() {
bindHandler();
});
In reading through the following JavaScript, a natural question is "why is the code so bare-metal? Couldn't most of this be accomplished in fewer lines using more jQuery?" An important consideration when implementing JavaScript in Splunk is that many enterprise customers are locked in to certain browsers that may be incompatible with some core jQuery code.
To be more specific, in testing this and other custom JavaScript, convenience functions like $jQuery.each() and certain jQuery selectors flat out don't work in IE7/8, and even worse can break a view's other JavaScript and CSS! In other words, Splunk has love for jQuery, but certain standards-based browsers do not have love for jQuery :(
The bindHandler() method, called by document.ready() above, is responsible for binding browser 'click' events on DOM elements that contain specific classes ('add-text', 'remove-text', and 'preview') to our special event handlers that are defined a bit further down in the file:
function bindHandler() {
$('.add-text').bind('click',function(event){addHandler(event);});
$('.remove-text').bind('click',function(event){removeHandler(event);});
$('.preview').bind('click',function(event){previewHandler(event);});
}
These class names correspond to the 'add new', 'remove', and 'Preview' buttons that will allow users to manipulate the Web Intelligence App's stored eventtypes as well as preview changes to the eventtypes before they are stored.
The addHandler() function, bound to DOM elements containing the 'add-text' class by bindHandler(), is responsible for creating, binding, and inserting new elements into the proper place in the DOM. These elements will enable app admins to add additional terms to the eventtype definitions that are part of the Web Intelligence setup workflow.
NB: The in-line comments below are included for illustrative purposes only and are not found in the actual code.
// the addHandler() function receives a jQuery event object from bind() function addHandler(event) { // get the event target's parent and parent's parent container var par = $(event.target).parent(); var gpar = par.parent(); // locate the eventtype name from another element in the parent container var name = $(event.target).parent().find('.dynamic-text').attr('name'); /* what follows is a bare-metal factory that produces new 'dd', 'input', and * 'button' elements and populates them with the requisite attributes. * unfortunately, jQuery.live() doesn't work in quite a */ few browsers, so it is up to us to provide binding for the new elements var nwdd = document.createElement('dd'); var inpt = document.createElement('input'); inpt.setAttribute('class', 'dynamic-text'); inpt.setAttribute('type', 'text'); inpt.setAttribute('name', name); var remv = document.createElement('button'); remv.setAttribute('class', 'remove-text'); remv.innerHTML = 'remove'; $(remv).bind('click', function(event){removeHandler(event);}); var add = document.createElement('button'); add.setAttribute('class', 'add-text'); add.innerHTML = 'add new'; $(add).bind('click', function(event){addHandler(event);}); /* now that we have our new elements, we can insert them into the DOM * first we make the input and buttons children of our 'dd' element */ then, we append the 'dd' element to the grandparent container nwdd.appendChild(inpt); nwdd.appendChild(remv); nwdd.appendChild(add); gpar[0].appendChild(nwdd); // the target gets the remove() treatment since the container no longer // needs an 'add new' button (the new container will have the it) $(event.target).remove(); /* what follows is some logic-foo that checks to see if the original target's * parent element had no remove button (i.e. it was the only element container) */ and if that is the case, a remove button is added to the parent container if (par.children('button').length < 1) { var remv = document.createElement('button'); remv.setAttribute('class', 'remove-text'); remv.innerHTML = 'remove'; $(remv).bind('click', function(event){removeHandler(event);}); par.get(0).appendChild(remv); } // return false to break the default 'submit' action since we are in a form return false; }
The removeHandler() function, bound to DOM elements containing the 'remove' class by bindHandler(), is responsible for removing elements from the DOM (i.e. the antithesis of addHandler()).
NB: The in-line comments below are included for illustrative purposes only and are not found in the actual code.
// the removeHandler() function receives a jQuery event object from bind() function removeHandler(event) { /* * * Possible states: * 1) this is the second to last element * 2) this is the last of several elements * 3) this is the first element * */ // get the event target's parent and parent's parent container var par = $(event.target).parent(); var gpar = par.parent(); // locate the eventtype name from another element in the parent container var name = $(event.target).parent().find('.dynamic-text').attr('name'); // calculate the event target's ancestor count (don't include the parent) var ancs = gpar.children().length-1; // stash the type attribute of the next() element after the event.target var ntype = $(event.target).next().attr('type'); // proceed if the next type is 'button' or 'submit' (depends on browser) if (ntype === 'submit' || ntype === 'button') { // if there is only one ancestor, remove the previous container's // children that have the 'remove-text' class if (ancs === 1) { par.prev().children('.remove-text').remove(); } // create an 'add new' button, bind it, and append it to the previous container var add = document.createElement('button'); add.setAttribute('class', 'add-text'); add.innerHTML = 'add new'; $(add).bind('click', function(event){addHandler(event);}); par.prev().get(0).appendChild(add); // the next type is NOT 'button' or 'submit' (depends on browser) } else { // if there is only one ancestor, remove the next container's // children that have the 'remove-text' class if (ancs === 1) { par.next().children('.remove-text').remove(); } } // remove the event target's parent par.remove(); // return false to break the default 'submit' action since we are in a form return false; }
The preview() function, bound to DOM elements containing the 'preview' class by bindHandler(), is responsible for creating a search constructed by input element values and then passing it to the 'flashtimeline' view in a new window. This enables app admins to ensure that their eventtype setting are correct before committing them via 'Save'.
NB: The in-line comments below are included for illustrative purposes only and are not found in the actual code.
// the preview() function receives a jQuery event object from bind() function previewHandler(event) { var name; var value; var values = []; // grab the log_value, which must be prefixed to any preview search // string regardless of which of the preview buttons was clicked var log_value = document.getElementsByName('eventtype.web-traffic.search')[0].value; // get a list of the siblings of the event target var sibs = $(event.target).prev().get(0).childNodes; // iterate through the siblings' children looking for elements with the class // 'dynamic-text' and populate the array 'values' with the matching elements for (var i = 0; i < sibs.length; i++) { if (sibs[i].hasChildNodes()) { var nodes = sibs[i].childNodes; for (var j = 0; j < nodes.length; j++) { if (nodes[j].className === 'dynamic-text') { values.push(nodes[j]); } } } } // if we have values, grab the eventtype name (format is 'eventtype.<name>.<key>') // and pass it along with the original 'values' array to the internal function // _dynValFact(), which serializes the given elements into valid Splunk search terms if (values.length>0) { name = values[0].name.split('.')[1]; value = [log_value, _dynValFact(name, values)].join(' '); } else { /* * hmmm, no values found? There are two possible conditions: * 1) The user clicked on the 'web-traffic - if so, make the donuts * 2) We are not in 'dynamic mode', which means we can just grab the * already serialized textarea value, merge it with 'log_value', * and then make the donuts */ name = $(event.target).prev().attr('name'); if (name === 'eventtype.web-traffic.search') { value = log_value; } else { value = [log_value, $(event.target).prev().val()].join(' '); } } // sanity check that our search string has some length if (value.length > 0) { // instantiate a new Splunk.TimeRange range = new Splunk.TimeRange('-1d','now'); // tack '| head 20' on to the search string to prevent long running searches value = value.concat(' | head 20'); // instantiate a new Splunk.Search search = new Splunk.Search(value, range); // use Splunk.Search.sendToView() to send our search to the 'flashtimeline' view var options = {}; options['windowFeatures'] = this.DEFAULT_WINDOW_FEATURES; search.sendToView('flashtimeline', {}, false, true, options, 'webintelligence'); // return false to break the default 'submit' action since we are in a form return false; } else { return false; } // return false to break the default 'submit' action since we are in a form return false; }