-
Notifications
You must be signed in to change notification settings - Fork 10
IRIS UI Programming
This is a guide to extend the IRIS UI for your own need. For the extension, you should understand following:
- IRIS home page structure
- CSS structure
- JavaScript file structure
In this page, we will cover the above subjects in depth.
IRIS UI has just one HTML page: index.html. The index.html has following structure.
Section is also called ‘block’, which represents a big category that constitutes a page. A section consists of several subsections. One thing to note is that a subsection can be a member of multiple sections.
Section is defined within a ‘div’ with ‘header’ class.
<div class="header"> <h1>IRIS: <span class="subtitle">The Recursive SDN Controller</span></h1> <div class="nav"> <ul> <li><a class="home">Home</a></li> <li><a class="switches">Switches</a></li> <li><a class="devices">Devices</a></li> <li><a class="topology">Topology</a></li> </ul> </div> <!-- /nav --> </div>
To define a new section, you just add a new ‘a’ tag to the ‘div’ tag with ‘nav’ class. You should not forget to assign the section name to the ‘a’ tag. For example, to define a new section ‘VTN’, you should modify the above code as follows:
<div class="header">
<h1>IRIS: <span class="subtitle">The Recursive SDN Controller</span></h1>
<div class="nav">
<ul>
<li><a class="home">Home</a></li>
<li><a class="switches">Switches</a></li>
<li><a class="devices">Devices</a></li>
<li><a class="topology">Topology</a></li>
<li><a class="vtn">VTN</a></li>
</ul>
</div> <!-- /nav -->
</div>
Each subsection is defined within a ‘div’ with ‘content’ class. Let’s see the following codes.
<div class="content">
...
<div class="home switches switches">
<h1>Switches(<span id="snum">0</span>)</h1><a><!-- reload button --></a>
<div>
<table class="horizontal_table mark_4th_row">
<colgroup>
<col span="3" class="desc">
<col span="3" class="stat">
<col class="time">
</colgroup>
<thead>
<tr>
<th>DPID</th><th>IP Address</th><th>Vendor</th><th>Packets</th><th>Bytes</th>
<th>Flows</th><th>Connected Since</th>
</tr>
</thead>
<tbody template="switches">
<!-- switch items go here -->
</tbody>
</table>
</div>
</div>
...
</div> <!-- /content -->
The above example subsection definition begins with <div class=”home switches switches”>. It shows that the subsection ‘switches’ belongs to two sections: ‘home’, and ‘switches’. In the subsection definition, there are HTML codes for the basic rendering of the subsection. Basically, the above code shows a table that consists of seven columns. However, you can also easily see that the actual data of the table is not given. It needs to be dynamically rendered. The area that the dynamically-loaded data goes is marked by ‘template’ attribute.
<tbody template="switches"> <!-- switch items go here --> </tbody>
That’s the <div class=”content”> structure. <div class=”content”> is a place that you define your subsections. Then you might ask, ‘where the hidden subsections go?’ OK. hidden subsections are also subsections, but they are not shown until you click somewhere. So, they are all defined within a hidden div <div class=”content”> which is right below the <div class=”content”>.
Let’s see one hidden subsection example.
<div id="hiddens" style="display: none;">
...
<div class="home switches ports" style="display: none;">
<table class="horizontal_table mark_2th_row">
<caption><span>Ports:</span><span type="links"></span></caption>
<thead>
<tr>
<th>#</th><th>Link Status</th><th>TX Bytes</th><th>RX Bytes</th><th>TX Pkts</th>
<th>RX Pkts</th><th>Dropped</th><th>Errors</th>
</tr>
</thead>
<tbody template="ports">
</tbody>
</table>
</div>
...
</div> <!-- /hiddens -->
You can also think this as a basic HTML code that is shown at screen when you click somewhere. But also you can easily see that there is a placeholder for the dynamically-loaded data.
<tbody template="ports"> </tbody>
OK. That’s it. At the later part of the HTML code is rather simple and you can guess what it does very easily. However, there is one last convention that you need to know. As you might already understand, JavaScript is the key technology that glue this HTML codes with the dynamically loaded data. So you MUST link your JavaScript code with this page whenever you add a new subsection.
<script src="js/periscope_controller_statistics.js"></script> <script src="js/periscope_switches.js"></script> <script src="js/periscope_devices.js"></script> <script src="js/periscope_topology.js"></script> <script src="js/periscope_flows.js"></script> <script src="js/periscope_ports.js"></script> <script src="js/periscope_switch_desc.js"></script>
When you add your JavaScript code, conventionally, you place the subsection name at the end of the file. For example, the javascript code for ‘switches’ subsection goes ‘periscope_switches.js’.
OK. That’s just all you need to know about the index.html structure. Before going into the JavaScript code, let’s see some of the CSS codes that need to be modified when you add a new section to your page.
All the CSS code for IRIS UI is in periscope.css in css directory. In the file you can easily find the following code.
.home .nav ul li a.home,
.switches .nav ul li a.switches,
.devices .nav ul li a.devices,
.topology .nav ul li a.topology
{
margin-top: 0;
background-color: white;
opacity: 1;
}
To define a new section to the IRIS page, you should modify the group selector. For example, when you add a new section ‘vtn’, you modify the above group selector as follows:
.home .nav ul li a.home,
.switches .nav ul li a.switches,
.devices .nav ul li a.devices,
.topology .nav ul li a.topology,
.vtn .nav ul li a.vtn
{
margin-top: 0;
background-color: white;
opacity: 1;
}
Before going into the detail, I’d like to first cover the periscope.js file first. Mostly you don’t need to modify this file, but if you added a new subsection that needs a new template (just like <tbody template=”switch_flows”></tbody>), you should add a code to preload the template when the index.html is loaded on your browser.
// periscope.js
...
$(document).ready( function() {
//load all templates to be used in this IRIS page.
iris.loadTemplates([
'controller_statistics', 'switches', 'devices', 'topology',
'switch_flows', 'ports', 'switch_desc'
]);
...
You should add you template name at the end of the list given to iris.loadTemplates method. And you need to place your template file in ‘tpl’ directory.
OK. As the basic prerequisite is covered, I’d like to move on to the next and the very important subject.
For the easy explanation, I will show you two examples for ‘switches’ subsection, and ‘desc’ hidden subsection. First, let’s see the ‘switches’ subsection example first. Maybe, this example is the most complicated one, so you might suffer from some difficulties in following my explanation which is very bored, but please be patient. :-)
In most cases, the JavaScript file follows the same pattern. Let’s see the first part of the periscope_switches.js file.
iris.switchCollection = new SwitchCollection();
...
iris.switches = function() {
iris.cancelAllTimersNotInBlock( $('body').attr('class') );
...
The first line is to load model associated with this presentation logic. Then, what is model? Model is a back-end data that pulls the actual data from IRIS controller via REST call. It modifies the data into the internal representation, and provides the abstracted data to the presentation logic (our JavaScript code). All the models are defined within ‘js/models’ directory. We will cover the details about the models later.
The third line is important. The line defines a function (handler) called when the ‘switches’ subsection appears on the screen. The name of the function, switches, MUST be equal to the name of the subsection.
In the next line, a function is called to cancel all the timers not associated with the section that the subsection ‘switch’ is placed. $(‘body’).attr(‘class’) is a code to retrieve the class name (that is, section name) assigned to the HTML ‘<body>’ tag. When the ‘home’ section is rendered at screen, the value is set to ‘home’. When the ‘switches’ section is rendered at screen, the value is set to ‘switches’. (Soon this call will be replaced by ‘iris.getCurrentBlockName()’)
One important thing that you should learn from this code is that every subsection can use timers to periodically do something. Let’s see the code in the same function.
iris.doTimers(['home', 'switches'], 'switches', 5000, function() {
...
// every period, we re-collect switch statistics
iris.switchCollection.fetch();
});
Let’s translate above code into natural language: “This timer belongs to ‘home’ and ‘switches’ section. And the name of this timer (actually the name of subsection) is ‘switches’. Every 5000 milliseconds, This timer execute a function that calls ‘iris.switchCollection.fetch()’. As this timer belongs to ‘home’ and ‘switches’ section, this timer would not be cancelled when the ‘home’ and ‘switches’ section is drawn at screen.
Anyway, the very important thing to know is that this code periodically update ‘switchCollection’ model in every 5 seconds by calling ‘switchCollection.fetch()’. Then, where is the code which do the rendering after the model update is done?
Let’s see the code at the very below of the above.
iris.switchCollection.on('add remove change', function() {
if ( !updating ) {
updating = true;
$('#snum').html( iris.switchCollection.length );
var target = $('tbody[template="switches"]');
iris.collectionTo(iris.switchCollection, target);
target.find('td a[type="id"]').each( function (index, x) {
$(x).click( function(event) {showSwitchPortInformation($(x).text());} );
});
target.find('td a[type="flows"]').each( function(index, x) {
$(x).click( function(event) {showSwitchFlowInformation($(x).attr('ref'));} );
});
target.find('td a[type="desc"]').each( function(index, x) {
$(x).click( function(event) {showSwitchDescInformation($(x).attr('ref'));} );
});
updating = false;
}
});
Let’s forget about the ‘updating’ flag for the moment.
The first line iris.switchCollection.on(‘add remove change’, function() { says “if ‘add’, ‘remove’, or ‘change’ event happens on the ‘switchCollection’ model, call this function”. Thus, above routine only be executed when the model data has actually been modified.
The code $(‘#snum’).html( iris.switchCollection.length ); updates the index.html to show the number of the switches. (You might remember that there was a tag identifier ‘snum’ in the subsection HTML code of ‘switches’.)
The code var target = $(‘tbody[template=”switches”]’); finds the target HTML element whose content would be rendered using ‘switches’ template and the dynamically loaded model.
The next line iris.collectionTo(iris.switchCollection, target); packs the model data into the target HTML element using the template. To understand what it does, you should understand the ‘switches’ template. Let’s see.
<tr> <td><a type="id"><%= id %></a></td> <td><%= inetAddress %></td> <td><a type="desc" ref="<%= id %>"><%= manufacturerDescription %></a></td> <td><%= packetCount %></td> <td><%= byteCount %></td> <td><a type="flows" ref="<%= id %>"><%= flowCount %></a></td> <td><%= connectedSince %></td> </tr> <tr ref="switch_<%= id %>"><td colspan="7"></td></tr>
Pretty simple, huh? You can think this HTML goes into the ‘$target’ HTML tag after all the <%= … %> parts are replaced by their actual value. One thing you should note is the type attribute of <a> tag. You can see there are an <a> tag whose type attribute value is desc. in the tag, you can also find the ref attribute whose value is switch id. The last line of the above HTML is the place where the ‘hidden’ subsection will be placed. I will talk about it soon.
The collectionTo method auto-loads the template whose name is the same with target.attr(‘template’). After that, for each of the Switch model in the switchCollection, replace all <%= … => with the fields in the Switch model, and append to target as a child element.
OK. That’s the basic procedures that most JavaScript code for subsection does. However, this ‘switches’ subsection example is much more complicated than that because there are three different kinds of hidden subsections are associated. In the above, you can see the following code.
target.find('td a[type="desc"]').each( function(index, x) {
$(x).click( function(event) {showSwitchDescInformation($(x).attr('ref'));} );
});
This code finds every link <a type=”desc”> within <td>, and add a click event handler to call showSwitchDescInformation with switch id as an argument to bring up the ‘hidden’ switch description subsection on the screen.
OK. I think now you can see the whole picture. Let’s move on to the actual handler, showSwitchDescInformation which is also defined within iris.switches function.
var showSwitchDescInformation = function(id) {
if ( updating ) {
return;
}
// find a space to insert the switch desc information.
var target = $('tr[ref$="' + id + '"] td');
if ( target.find('div').length > 0 ) {
return;
}
++stopTimer;
var source = $('#hiddens div.switch_desc').clone();
source.find('tbody')
.attr('ref', 'switch_desc_' + id);
target.addClass('chosen');
$('tr[ref$="' + id + '"]').prev().addClass('chosen');
source.appendTo(target).css('display', 'block');
$('tr.chosen a[type="desc"]').click( function(event) {
var link = $(event.target);
var p = link.parents('tr');
p.removeClass('chosen');
p.next().find('td.chosen').empty().removeClass('chosen');
link.click( function(e) {showSwitchDescInformation($(e.target).attr('ref'));} );
--stopTimer;
});
iris.switch_desc(id, source);
};
Quite long. So, let’s break down the code to show what each line does, skipping the rather unimportant details.
var target = $('tr[ref$="' + id + '"] td');
if ( target.find('div').length > 0 ) {
return;
}
This finds a tag to place the switch description information (now you know what it is). If there’s something inside, we just return to leave it as it is.
var source = $('#hiddens div.switch_desc').clone();
source.find('tbody').attr('ref', 'switch_desc_' + id);
Now we clone the source of the hidden subsection, and assign a new attribute ‘ref’ to the <tbody> inside. The value of ‘ref’ attribute is the concatenation of ‘switch_desc_’ string and the switch id. This attribute is used later to find the tag to place switch information inside.
target.addClass('chosen');
$('tr[ref$="' + id + '"]').prev().addClass('chosen');
source.appendTo(target).css('display', 'block');
Now we assign ‘chosen’ class to the target <td> and the previous sibling of target’s enclosing <tr> tag. This ‘chosen’ class is to show a red box around it (in the CSS file, there is a code for that), and to assign an event handler that hide the ‘chosen’ area when it is clicked.
And finally, the cloned HTML code is appended to the target HTML element as a child. To show the cloned HTML on screen, its CSS attribute ‘display’ is set to ‘block’.
$('tr.chosen a[type="desc"]').click( function(event) {
var link = $(event.target);
var p = link.parents('tr');
p.removeClass('chosen');
p.next().find('td.chosen').empty().removeClass('chosen');
link.click( function(e) {showSwitchDescInformation($(e.target).attr('ref'));} );
--stopTimer;
});
The above code is the event handler which is assigned to the link whose ‘type’ attribute is set to ‘desc’. The link is the same link that was originally used to kick showSwitchDescInformation to be started.
The above code, when it is executed, removes the switch information from the page, remove ‘chosen’ class from tags, and finally restore the click handler what was assigned to the link whose type was ‘desc’.
And here comes the last part.
iris.switch_desc(id, source);
Then, what the iris.switch_desc does? Let’s look into the periscope_switch_desc.js file.
iris.switch_desc = function(id, target) {
if ( ! id ){
// if id is undefined, just return
return;
}
var aSwitch = iris.switchCollection.get(id);
var obj = _.template(iris.tpl['switch_desc'], aSwitch.toJSON());
target.find('tbody').append($(obj));
};
It first finds a switch using switch id as a key. Then, using the underscore library, pack the switch information to the template. The returned result is a complete HTML code. Finally, it appends the HTML code under the <tbody> tag within ‘target’ element.
As the explanation for showSwitchDescInformation is done, let’s go back to the iris.switches function. It’s last line is:
iris.switchCollection.fetch();
So, when the ‘switches’ subsection is drawn to screen, the switchCollection is synchronized with the information within IRIS controller.
OK. That’s it. Now I think you understand the following:
- Every subsection has a javascript function with the same name (for example, iris.<subsection name>) regardless it’s hidden or not
- The Role of the javascript function iris.<subsection name> is to pack required information into the subsection, regardless it’s hidden or not. For that, if needed, you should synchronize your model with the controller.
- When a subsection requires interactions with hidden subsections, you need to write a rather complicated set of code. (this problem will be fixed in the next revision.)
As already mentioned, all the models are defined within ‘js/models’ directory. The role of each model is to synchronize their data with the controller’s data. Let’s see the ‘hostmodel.js’ as an example.
window.Host = Backbone.Model.extend({
defaults: {
lastSeen: 'never',
ip: ' ',
swport: ' ',
},
});
window.HostCollection = Backbone.Collection.extend({
model:Host,
initialize:function () {
var self = this;
//console.log("fetching host list")
$.ajax({
url:hackBase + "/wm/device/all/json",
dataType:"json",
success:function (data) {
self.reset();
_.each(data, function(h) {
if (h['attachmentPoint'].length > 0) {
h.id = h.mac[0];
h.swport = _.reduce(h['attachmentPoint'], function(memo, ap) {
return memo + ap.switchDPID + "-" + ap.port + " "}, "");
h.lastSeen = new Date(h.lastSeen).toLocaleString();
self.add(h, {merge: true, silent: true});
}
});
self.trigger('add'); // batch redraws
}
});
},
fetch:function () {
this.initialize();
}
});
This defines two models: Host and HostCollection. The HostCollection is a collection of Hosts.
The collection loads data from controller by doing ajax on /wm/device/all/json. When the ajax call succeeds, it add each host information to the collection by calling self.add method after parsing each item of the list using the _.each method (in the ‘underscore’ library). After all the item is added, it calls self.trigger to notify the user of this model to handle the event ‘add’. You already have seen how to catch this event in your JavaScript function:
iris.switchCollection.on('add remove change', function() {
...
});
The fetch just calls initialize, so the two method does the same thing.
One thing to note is that you can access each model of the collection as follows:
_.each(collection.models, function(model) {
// do something with the model
});
You can find the usage example of this model within the ‘periscope_devices.js’ file. The general usage guideline of the model collection can be found in the collectionTo method defined in the ‘periscope.js’ file.
OpenIRIS Development Team: contact bjlee@etri.re.kr