Gridmap Generic Developer Guide
Client Side Gridmap
Here you can find new approach to gridmaps used to create new version of cms critical services monitoring page. It uses
InfoVis framework for displaying the gridmap chart.
Introduction
Welcome to
Gridmap Generic Developer Guide. This document intends to help people familiarize with a gridmap software and help them build their own applications. It includes:
- Adding UI elements and dynamic UI generation
- Adding events to UI controls
- Adding Ajax calls
- Integrating RSH
(Really Simple History)
- Reading data from URL
All above topics are common to gridmap development and should help with familiarizing with GM's MVC pattern and data flow. For more information or/and repository access contact
Max Boehm (
max.boehm@edsNOSPAMPLEASE.com) or
James Casey (
James.Casey@cernNOSPAMPLEASE.ch). Application with example code implemented can be found in gridmap repository under
reusable-parts/example app/gridmap-generic-dev-guide-example. If you want more documentation about gridmap software checkout repository
doc and
app/gridmap/doc directories.
Required knowledge
There are few technologies that are widely used in a gridmap software:
- Python
- Advanced JavaScript (functional programming style, variable scope and lifetime, concept of "closures", ...)
- Model-View-Controller concept, AJAX concept, JSON, concept of dynamic HTML, CSS2
Here are some resources useful to review:
Gridmap Generic application structure
Directory |
File Name |
Description |
client/ |
index.html |
Main *.html file |
client/ |
mygmdata.js |
File that implements MODEL. All of the data used by gridmap should be stored in here. |
client/ |
mygmview.js |
File that implements VIEW. It is responsible for drawing the gridmap and the tooltips. |
client/ |
mygmcontrol.js |
File that implements CONTROLLER. Initialization, UI Events, direct DOM references all should be implemented here. |
client/ |
myservice.py |
File used to mediate between client and server sides of the application. |
client/ |
style.css |
main CSS (Cascading Style Sheets) file, used to set up UI looks. |
server/gridmapgeneric/ |
__init__.py |
Initialization file needed by python interpreter. |
server/gridmapgeneric/ |
init.py |
Gridmap Generic initialization file. Sets up *.cfg file to use. Loads settings from *.cfg file (eg. database connection data) |
Gridmap client resources flow
Typical gridmap client inner communication diagram:
- MVC pattern is created in index.html file. Main HTML file may also use some functions located in mygmview.js file.
- All the data stored in MyGMData along with it's methods are visible in MyGMControl class.
- MyGMData uses methods from MyGMView to eg. redraw gridmap.
Adding UI elements and dynamic UI generation
Probably the first thing that needs to be done is the user interface. All UI controls can be added inside index.html file. There are few controls that are commonly used in gridmaps:
- Buttons
- Selects (drop down lists)
- Checkboxes
For more information about HTML UI controls and HTML in general check out
http://www.w3schools.com/
.
As an example, site selection interface will be added. Sites will be grouped by tier membership. Tiers will be selectable by proper buttons and sites by dynamically generated dropdown list.
First, index.html needs to be edited, the following lines should be added:
Tier <input class="button-tier" type="button" value="0" id="tier0" />
<input class="button-tier" type="button" value="1" id="tier1" />
<input class="button-tier" type="button" value="2" id="tier2" />
Site <select id="site-select"></select>
This will display UI controls. They don't have any actions attached to them and select field is empty right now.
Next we should define model functions and variables from which for eg. sites drop down list will be generated:
// sitesGrouped variable definition
this.sitesGrouped = [
["CERN-PROD"],
["FZK-LCG2","IN2P3-CC","INFN-T1","NIKHEF-ELPROD","RAL-LCG2","Taiwan-LCG2","USCMS-FNAL-WC1","pic"],
["BEIJING-LCG2","BEgrid-ULB-VUB","BUDAPEST","BelGrid-UCL","CIEMAT-LCG2","CIT_CMS_T2","CSC","CSCS-LCG2","DESY-HH","GLOW-CMS","GR-05-DEMOKRITOS","GRIF","HEPGRID_UERJ","Hephy-Vienna","IFCA-LCG2","IN2P3-CC-T2","IN2P3-IRES","IN2P3-LAPP","IN2P3-LPC","IN2P3-SUBATECH","INDIACMS-TIFR","INFN-BARI","INFN-LNL-2","INFN-NAPOLI-CMS","INFN-PISA","INFN-ROMA1-CMS","INFN-TRIESTE","ITEP","JINR-LCG2","Kharkov-KIPT-LCG2","LCG_KNU","LIP-Coimbra","LIP-Lisbon","MIT_CMS","Nebraska","Purdue-RCAC","RRC-KI","RU-Protvino-IHEP","RWTH-Aachen","SPRACE","T2_Estonia","TR-03-METU","TR-10-ULAKBIM","TW-FTT","UCSDT2","UFlorida-HPC","UKI-LT2-Brunel","UKI-LT2-IC-HEP","UKI-LT2-IC-LeSC","UKI-LT2-QMUL","UKI-LT2-RHUL","UKI-SOUTHGRID-BRIS-HEP","UKI-SOUTHGRID-RALPP","WARSAW-EGEE","ru-Moscow-SINP-LCG2","ru-PNPI"]
];
// function that returns sorted list of sites for a given tier
this.givTierSites = function() {
var tier = this.givSiteTier();
return this.sitesGrouped[tier].sort();
};
// function that sets up first site on a given tier after tier number has been changed
this.setTierSite = function(tier) {
this.site = this.sitesGrouped[tier][0];
this.setDirty();
};
// function that gives current tier number based on given site name
this.givSiteTier = function() {
for (var i=0; i < this.sitesGrouped.length; i++) {
for (var j=0; j < this.sitesGrouped[i].length; j++) {
if (this.sitesGrouped[i][j] == this.site) {
return i;
}
}
}
};
// function that sets up site name
this.setSite = function(site) {
this.site = site;
this.setDirty();
};
Above code should be paste into mygmdata.js file into MyGMData class object. All that needs to be done right now is setting up events and actions for them in MyGMControl class (mygmcontrol.js). In MyGMControl class
initialize() function can be found, edit it to look like this:
// Sets up starting parameters, run necessary ajax calls
this.initialize = function()
{
// mygmcontrol class reference
// use examples:
// EXAMPLE 1: $('site-select').addEvent("change", function() { thisRef.changeSite(this.value) }); // Adding Event
// EXAMPLE 2: ajax_getsites(function(obj){ thisRef.updateSitesGrouped(obj.allsites); }); // Running ajax call
var thisRef = this;
// INITIALIZE MODEL
// code to initialize gmdata...
// "template" = name of the initial map (must be configured in the server)
// "100" = row layout
// 60 = refresh page every 60 seconds
gmdata.setMap("template", "100", 60);
gmdata.setSite("CERN-PROD"); // sets up default site
submap1.setSite("CERN-PROD"); // sets up default site
submap2.setSite("CERN-PROD"); // sets up default site
gmdata.setSubmap(submap1); // optional
submap1.setSubmap(submap2); // optional
if (param("test") == true) {
gmdata.setUpdateCallback(showdata);
submap1.setUpdateCallback(showdata);
submap2.setUpdateCallback(showdata);
}
// EVENTS
// add events to sites drop down list
$('site-select').addEvent("change", function() { thisRef.changeSite(this.value) });
// add events to tiers buttons
$$('input.button-tier').addEvent("click", function() { thisRef.changeTier(this.value) });
// update UI controls
this.updateUI();
// DRAW GRIDMAP
gmdata.updateGridmap();
};
What is important in the above example is events adding routine. Initialize function is the only proper place to implement events for all of the UI Elements.
Now add the following functions to the MyGMControl class, preferably above
initialize() function.
// This function is used on click on tier buttons
// it sets up tier and as default first of its sites and update UI
// val - tier number (0, 1, 2)
this.changeTier = function(val) {
gmdata.setTierSite(val);
submap1.setTierSite(val);
submap2.setTierSite(val);
this.updateUI();
gmdata.updateGridmap();
};
// This function is used on site change
// it sets up site new site name and update UI
// val - sitename (eg. CERN-PROD, FZK-LCG2 etc.)
this.changeSite = function(val) {
gmdata.setSite(val);
submap1.setSite(val);
submap2.setSite(val);
this.updateUI();
gmdata.updateGridmap();
};
// ------------------------------------------------------
// Functions to update UI state to match current settings
// ------------------------------------------------------
// update the whole UI state according to the settings in gmdata
this.updateUI = function()
{
// code to update the UI to reflect the state of the model
// (here the selected sitename can be read from the model and the corresponding
// <option> of the dropdown field can be selected)
// select tier (makes it bold)
$$('input.button-tier').setStyle("font-weight", "normal");
$('tier'+gmdata.givSiteTier()).setStyle("font-weight", "bold");
// draw sites list
var selectElement = $("site-select");
var sites = gmdata.givTierSites();
selectElement.empty();
for (var i=0; i < sites.length; i++) {
var sitename = sites[i];
if (sitename == gmdata.site) {
option = new Element('option', {'value': sitename, 'selected': 'selected'});
}
else {
option = new Element('option', {'value': sitename});
}
option.setText(sitename);
option.injectInside(selectElement);
}
};
If everything was done correctly the UI controls should work now. They are not directly connected to the gridmap so they can't change its state, but implementing such functionality is not a hard thing to do. All that needs to be done is setting up the script so it could send eg. site value through ajax call and depending on this value server could generate gridmap for a given site.
Adding Ajax calls
In this section the way to implement a new Ajax call will be presented. Right now, the site list is generated from a hard coded list that is located in MyGMData class object. What needs to be done is a new ajax call that will get the same data from a server side script. That data will be then used to generate sites drop down list. At the beginning server side script
myservice.py should be modify to fit new expectations. Inside includes section add line
from gridmapvositeview.init import _cache
this will import cache object instance that will be used to store objects for faster second use. Next paste the following code:
sitesGrouped = [
["CERN-PROD"],
["FZK-LCG2","IN2P3-CC","INFN-T1","NIKHEF-ELPROD","RAL-LCG2","Taiwan-LCG2","USCMS-FNAL-WC1","pic"],
["BEIJING-LCG2","BEgrid-ULB-VUB","BUDAPEST","BelGrid-UCL","CIEMAT-LCG2","CIT_CMS_T2","CSC","CSCS-LCG2","DESY-HH","GLOW-CMS","GR-05-DEMOKRITOS","GRIF","HEPGRID_UERJ","Hephy-Vienna","IFCA-LCG2","IN2P3-CC-T2","IN2P3-IRES","IN2P3-LAPP","IN2P3-LPC","IN2P3-SUBATECH","INDIACMS-TIFR","INFN-BARI","INFN-LNL-2","INFN-NAPOLI-CMS","INFN-PISA","INFN-ROMA1-CMS","INFN-TRIESTE","ITEP","JINR-LCG2","Kharkov-KIPT-LCG2","LCG_KNU","LIP-Coimbra","LIP-Lisbon","MIT_CMS","Nebraska","Purdue-RCAC","RRC-KI","RU-Protvino-IHEP","RWTH-Aachen","SPRACE","T2_Estonia","TR-03-METU","TR-10-ULAKBIM","TW-FTT","UCSDT2","UFlorida-HPC","UKI-LT2-Brunel","UKI-LT2-IC-HEP","UKI-LT2-IC-LeSC","UKI-LT2-QMUL","UKI-LT2-RHUL","UKI-SOUTHGRID-BRIS-HEP","UKI-SOUTHGRID-RALPP","WARSAW-EGEE","ru-Moscow-SINP-LCG2","ru-PNPI"]
];
def getSites():
key = "sites" # set up cache key
obj = _cache.get(key) # try to load sites list from cache
if not obj:
obj = sitesGrouped # if not successful load it from local variable
if len(obj):
_cache.add(key, obj, 3600) # Adds object to cache
return obj
Newly created
getSites() function will be used by ajax call for retrieving sites list. Because caching capability is presented in this example init.py file modification is needed. First include caching library:
import common.cache as cache
And initialize cache, preferably after *.cfg file handling
# create new cache
_cache = cache.Cache(100)
_cache_server_timeout = 30
Restart the server and try out if new functionality works by accessing
http://path-to-your-script/myservice.py/getSites. If sites list will be displayed it means that everything works fine.
Integrating RSH (Really Simple History)
Using Ajax have one basic flaw, browser can not store history. In addition to that, bookmarking capability is very useful in a gridmap applications. There are many scripts that could be implemented in order to provide this functionality, but authors weapon of choice will be RSH library. RSH is present in the common package, so it can be included in index.html file in head section:
<script type="text/javascript" src="/lib/rsh.js"></script>
And create a blank.html file with no contents. Next step will be to initialize the library:
// initialize history/bookmarking script
window.dhtmlHistory.create({
toJSON: function(o) {
return Json.toString(o);
},
fromJSON: function(s) {
return Json.evaluate(s);
},
debugMode: false
});
Above code can be inserted right before the following line in index.html file:
window.addEvent('domready', function()
Few changes are needed in mygmcontrol.js file. First add necessary functions into control class:
// This function is used to update gridmap along with UI on history event
this.setFromHistoryString = function() {
gmdata.setSite(param("site"));
submap1.setSite(param("site"));
submap2.setSite(param("site"));
if (gmdata.site) {
this.updateUI();
gmdata.updateGridmap();
}
};
// update URL hash
this.updateUrlHash = function() {
dhtmlHistory.add("site="+gmdata.site, 1);
};
And add line
this.updateUrlHash();
into changeTier() and changeSite() functions just after
this.updateUI();
. Then modify initialize() function. Add history listener after UI update invocation.
// add history listner
dhtmlHistory.addListener( function() { thisRef.setFromHistoryString(); });
Last thing that needs to be done is small mygmdata.js file edit. At a bottom of this file the following piece of code can be found:
// parse parameters from URL and make them available as globals param("name")
(function()
{
var s = window.location.search.substring(1).split('&');
var c = {};
for (var i = 0; i < s.length; i++) {
var parts = s[i].split('=');
c[unescape(parts[0])] = unescape(parts[1]);
}
window.param = function(name) { return name ? c[name] : c; };
})();
Replace it with:
// parse parameters from URL and make them available as globals param("name")
function param(name)
{
var currentHash = dhtmlHistory.getCurrentHash().split('&');
var param = {};
for (var i = 0; i < currentHash.length; i++) {
var parts = currentHash[i].split('=');
param[unescape(parts[0])] = unescape(parts[1]);
}
return name ? param[name] : param;
}
This is needed because RSH script uses URL hash as a variables holder. It also needs to be loaded every time when history event occurs. Of course there are lots of possible methods for implementing this so feel free to experiment.
F.A.Q.
How to omit showing information in tooltips?
If you, for example, want to omit displaying one or more columns in certain situations do something like this:
tipOutput += '<table id="tooltip_dataTable" cellpadding="0" cellspacing="1">';
tipOutput += '<tr>';
if (dispName) tipOutput += '<td class="tblHeader"> Name </td>';
tipOutput += '<td class="tblHeader center"> Status </td>';
if (dispValue) tipOutput += '<td class="tblHeader center"> Value </td>';
if (dispTarget) tipOutput += '<td class="tblHeader center"> Target </td>';
if (dispUrl) tipOutput += '<td class="tblHeader center"> Go to </td>';
tipOutput += '</tr>';
How to create loading information for submap?
If you have performance problems during displaying submaps you can add loading info to your gridmap application.
First thing is to add div with your message to your gridmap html (best way is to put it at the end of html file but inside body tag) file, that way you will be able to change your message text or looks without going thru js code:
<div id="loadingMessage" style="position:absolute;display:none; color: #FFFFFF; font-size: 8pt; font-weight:bold; background-color: gray"> Constructing a submap can take few seconds, be patient... </div>
Modify MyGMView.show() function in mygmview.js file like this:
// show the <div> element
this.show = function() {
$('loadingMessage').setStyle('display', 'none'); // hide loading info when submap in displayed
$(canvas).setStyle('visibility', 'visible');
};
Add function that displays loading screen when needed (in MyGMView class):
// show submap loading screen
this.showLoadingInfo = function(x, y) {
$('loadingMessage').setStyle('display', 'inline');
$('loadingMessage').setStyle('top', y+'px');
$('loadingMessage').setStyle('left', x+'px');
};
Modify MyGMView.update.myclick() function in mygmview.js file like this:
var myclick = function(gmdata, name, metric, rightclick) {
var msg;
// set the global handler function _GM which is invoked from "<a href='javascript:_GM(..)'>..</a>"
// when the user clicks on a link in a tooltip (function has access to gmdata via closure!)
_GM = function(url, param2) {
// open external url in a new window
window.open(url, param2||"Data").focus();
};
if (metric.Submap && !rightclick) { // decide if a drill down gridmap shall be displayed
// drill down
if (gmdata.submap) {
// create a hovering submap
gmdata.submap.setMap(gmdata.mapname+"|"+name);
if (gmdata.submap.gmview.popup) {
gmdata.submap.gmview.setPosition(tt_x-30, tt_y-30); // tt_x, tt_y is the current mouse position
thisRef.showLoadingInfo(tt_x-30, tt_y-30); // <- this line was added
}
gmdata.submap.updateGridmap(); // redraw
} else {
gmdata.drillInOut(name);
gmdata.updateGridmap(); // redraw
}
} else {
msg = mytip(gmdata, name, metric, true);
Tip(msg, TITLE, name, DELAY, 0, STICKY, true, OFFSETX, -10, OFFSETY, -10);
}
};
Loading message will be shown during server response time
Adding/Deleting/Editing status names and colors?
To add another possible status you have to edit 3 files: mygmview.js, style.css and your gridmap *.html file
In mygmview.js file edit function mycolor_status(status), in it, you will find object with status-colour pairs. Simply add/edit/delete pairs as you see fit.
In your html file find gridmap_colorkey_status() function invocation (it should be at the bottom of the file) and edit it.
In style.css file you will find status colour definitions eg.:
.bg-maint { background-color: #ddbb99; }
.bg-na { background-color: #eeeeee; }
.bg-inactive { background-color: #cccccc; }
.bg-poor { background-color: #ff0000; }
.bg-degraded { background-color: #ffff00; }
.bg-ok { background-color: #00ff00; }
.col-poor { color: #ff0000; }
.col-ok { color: #00cc00; }
Edit it to match your previous changes
How to make tooltip to display 0 (zero) value?
In mytip functions located in mygmview.js file you will often see construct like this:
(a.Value||"")
. It prevents script from displaying "undefined" or things like that, when value is somehow wrong, but it has one flaw.
When value is 0 (zero) and you want to display it, you can't. to solve this instead of
(a.Value||"")
construct write something like this:
(a.Value||(a.Value == 0 ? '0' : ''))
.
Dynamicly generated select options
Sometimes it is necessary to generate html elements dynamically on the fly, here is example loop from gridmap-vo-siteview application:
for (var i=0; i < sites.length; i++) {
var sitename = sites[i];
if (sitename == gmdata.site) {
option = new Element('option', {'value': sitename, 'selected': 'selected'});
}
else {
option = new Element('option', {'value': sitename});
}
option.setText(sitename);
option.injectInside(selectElement);
}
What is important here is Mootools Element() and injectInside() functions. First is used to create DOM element, second to inject created option to select element. setText function is used to set label to options element.
--
LukaszKokoszkiewicz - 11 May 2009