HTRACE-77. htraced gui: add pagination to the search page. (abe via cmccabe)
diff --git a/LICENSE.txt b/LICENSE.txt
index 1d80bc4..78b178a 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -244,10 +244,18 @@
 Jeremy Ashkenas, DocumentCloud. It is MIT licensed:
 https://github.com/jashkenas/backbone/blob/master/LICENSE
 
+backbone-paginator, is a javascript library, that is Copyright (c) 2012-2014
+Jimmy Yuen Ho Wong and contributors. It is MIT licensed:
+https://github.com/backbone-paginator/backbone.paginator/blob/master/LICENSE-MIT
+
 backgrid, is a javascript library, that is Copyright (c) 2013
 Jimmy Yuen Ho Wong. It is MIT licensed:
 https://github.com/wyuenho/backgrid/blob/master/LICENSE-MIT
 
+backgrid-paginator, is a javascript library, that is Copyright (c) 2013
+Jimmy Yuen Ho Wong. It is MIT licensed:
+https://github.com/wyuenho/backgrid-paginator/blob/master/LICENSE-MIT
+
 moment.js is a front end time conversion project.
 It is (c) 2011-2014 Tim Wood, Iskren Chernev, Moment.js contributors
 and shared under the MIT license:
diff --git a/htrace-core/src/web/app/mock.js b/htrace-core/src/web/app/mock.js
deleted file mode 100644
index e4277e5..0000000
--- a/htrace-core/src/web/app/mock.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
- 
-var spans = new App.Spans([
-  {"beginTime": 1419977682, "stopTime": 1419977689, "description": "test1()", "spanId": 1, "traceId": 1, "parentSpanId": null, "processId": "namenode:1"},
-  {"beginTime": 1419977685, "stopTime": 1419977690, "description": "test2()", "spanId": 2, "traceId": 2, "parentSpanId": 1, "processId": "datanode:1"}
-]);
diff --git a/htrace-core/src/web/app/models/span.js b/htrace-core/src/web/app/models/span.js
index e292970..85c7c72 100644
--- a/htrace-core/src/web/app/models/span.js
+++ b/htrace-core/src/web/app/models/span.js
@@ -53,33 +53,27 @@
   }
 });
 
-App.Spans = Backbone.Collection.extend({
+App.Spans = Backbone.PageableCollection.extend({
   model: App.Span,
+  mode: "client",
+  state: {
+    pageSize: 2
+  },
   url: "/query",
 
-  initialize: function(models, options) {
-    this.predicates = [];
-    return Backbone.Collection.prototype.initialize.apply(this, arguments);
-  },
-
-  fetch: function(options) {
-    options = options ? _.clone(options) : {};
-    options.data = {
-      "query": {
-        "lim": 100000
-      }
+  query: function(options, predicates) {
+    var query = {
+      "lim": 100000
     };
 
-    if (this.predicates.length > 0) {
-      options.data.query.pred = this.predicates;
+    if (predicates && predicates.length > 0) {
+      query.pred = predicates;
     }
 
-    options.data.query = JSON.stringify(options.data.query);
+    options = options ? _.clone(options) : {};
+    options.data = options.data ? _.clone(options.data) : {};
+    options.data.query = JSON.stringify(query);
 
-    return Backbone.Collection.prototype.fetch.apply(this, [options]);
-  },
-
-  setPredicates: function(predicates) {
-    this.predicates = predicates;
+    return this.fetch(options);
   }
 });
diff --git a/htrace-core/src/web/app/setup.js b/htrace-core/src/web/app/setup.js
index bb41ad8..9b78ba3 100644
--- a/htrace-core/src/web/app/setup.js
+++ b/htrace-core/src/web/app/setup.js
@@ -26,7 +26,7 @@
 
   initialize: function() {
     this.spansCollection = new App.Spans();
-    this.spansCollection.fetch();
+    this.spansCollection.query();
 
     this.spanViews = {};
 
diff --git a/htrace-core/src/web/app/views/search.js b/htrace-core/src/web/app/views/search.js
index bcdd980..4e221ed 100644
--- a/htrace-core/src/web/app/views/search.js
+++ b/htrace-core/src/web/app/views/search.js
@@ -34,7 +34,7 @@
     var endtime = $(this.el).find("#stoptime").val() || now.format("H:mm A");
     var duration = $(this.el).find("#duration").val();
 
-    var newSpans = spans.clone();
+    var newSpans = spans.fullCollection;
 
     if (begindate) {
       begin = new moment(begindate + " " + begintime).unix();
@@ -78,8 +78,7 @@
       });
     }
 
-    this.collection.setPredicates(predicates);
-    this.collection.fetch();
+    this.collection.query(null, predicates);
 
     return false;
   }
diff --git a/htrace-core/src/web/app/views/span.js b/htrace-core/src/web/app/views/span.js
index dd50bc6..764b797 100644
--- a/htrace-core/src/web/app/views/span.js
+++ b/htrace-core/src/web/app/views/span.js
@@ -46,7 +46,6 @@
   }
 });
 
-
 App.ListSpansView = Backbone.View.extend({
   "tagName": "div",
 
@@ -75,15 +74,23 @@
         }
       })
     });
+
+    this.listSpansPaginator = new Backgrid.Extension.Paginator({
+      collection: this.collection
+    });
   },
 
   "render": function() {
     $(this.listSpansView.$el).detach();
+    $(this.listSpansPaginator.$el).detach();
 
     this.listSpansView.render();
+    this.listSpansPaginator.render();
 
     $(this.$el).append(this.listSpansView.$el);
+    $(this.$el).append(this.listSpansPaginator.$el);
 
     return this;
   }
-});
\ No newline at end of file
+});
+
diff --git a/htrace-core/src/web/index.html b/htrace-core/src/web/index.html
index 181762f..7f0773c 100644
--- a/htrace-core/src/web/index.html
+++ b/htrace-core/src/web/index.html
@@ -25,7 +25,8 @@
     <!-- TODO: Add Favicon -->
     <link rel="icon" href="//favicon.ico" type="image/x-icon" sizes="16x16">
     <link href="lib/bootstrap-3.3.1/css/bootstrap.css" rel="stylesheet">
-    <link href="lib/css/backgrid.min.css" rel="stylesheet">
+    <link href="lib/css/backgrid-0.3.5.min.css" rel="stylesheet">
+    <link href="lib/css/backgrid-paginator-0.3.5.min.css" rel="stylesheet">
     <link href="lib/pickadate-3.5.2/themes/classic.css" rel="stylesheet">
     <link href="lib/pickadate-3.5.2/themes/classic.date.css" rel="stylesheet">
     <link href="lib/pickadate-3.5.2/themes/classic.time.css" rel="stylesheet">
@@ -159,10 +160,12 @@
     </script>
 
     <script src="lib/js/jquery-2.1.3.min.js" type="text/javascript"></script>
+    <script src="lib/bootstrap-3.3.1/js/bootstrap.min.js" type="text/javascript"></script>
     <script src="lib/js/underscore-1.7.0.min.js" type="text/javascript"></script>
     <script src="lib/js/backbone-1.1.2.min.js" type="text/javascript"></script>
-    <script src="lib/bootstrap-3.3.1/js/bootstrap.min.js" type="text/javascript"></script>
-    <script src="lib/js/backgrid.min.js" type="text/javascript"></script>
+    <script src="lib/js/backbone.paginator-2.0.2.min.js" type="text/javascript"></script>
+    <script src="lib/js/backgrid-0.3.5.min.js" type="text/javascript"></script>
+    <script src="lib/js/backgrid-paginator.js" type="text/javascript"></script>
     <script src="lib/js/moment-2.9.0.min.js" type="text/javascript"></script>
     <script src="lib/pickadate-3.5.2/picker.js" type="text/javascript"></script>
     <script src="lib/pickadate-3.5.2/picker.date.js" type="text/javascript"></script>
diff --git a/htrace-core/src/web/lib/css/backgrid.min.css b/htrace-core/src/web/lib/css/backgrid-0.3.5.min.css
similarity index 100%
rename from htrace-core/src/web/lib/css/backgrid.min.css
rename to htrace-core/src/web/lib/css/backgrid-0.3.5.min.css
diff --git a/htrace-core/src/web/lib/css/backgrid-paginator-0.3.5.min.css b/htrace-core/src/web/lib/css/backgrid-paginator-0.3.5.min.css
new file mode 100644
index 0000000..285fe81
--- /dev/null
+++ b/htrace-core/src/web/lib/css/backgrid-paginator-0.3.5.min.css
@@ -0,0 +1 @@
+.backgrid-paginator{text-align:center;border-top:0;-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.backgrid-paginator ul{display:inline-block;*display:inline;margin:5px 0;*zoom:1}.backgrid-paginator ul>li{display:inline}.backgrid-paginator ul>li>a,.backgrid-paginator ul>li>span{float:left;width:30px;height:30px;padding:0;line-height:30px;text-decoration:none}.backgrid-paginator ul>li>a:hover,.backgrid-paginator ul>.active>a,.backgrid-paginator ul>.active>span{background-color:#f5f5f5}.backgrid-paginator ul>.active>a,.backgrid-paginator ul>.active>span{color:#999;cursor:default}.backgrid-paginator ul>.disabled>span,.backgrid-paginator ul>.disabled>a,.backgrid-paginator ul>.disabled>a:hover{color:#999;cursor:default}
\ No newline at end of file
diff --git a/htrace-core/src/web/lib/js/backbone.paginator-2.0.2.min.js b/htrace-core/src/web/lib/js/backbone.paginator-2.0.2.min.js
new file mode 100644
index 0000000..687349c
--- /dev/null
+++ b/htrace-core/src/web/lib/js/backbone.paginator-2.0.2.min.js
@@ -0,0 +1,8 @@
+/*
+  backbone.paginator 2.0.0
+  http://github.com/backbone-paginator/backbone.paginator
+
+  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+  Licensed under the MIT @license.
+*/
+!function(a){if("object"==typeof exports)module.exports=a(require("underscore"),require("backbone"));else if("function"==typeof define&&define.amd)define(["underscore","backbone"],a);else if("undefined"!=typeof _&&"undefined"!=typeof Backbone){var b=Backbone.PageableCollection,c=a(_,Backbone);Backbone.PageableCollection.noConflict=function(){return Backbone.PageableCollection=b,c}}}(function(a,b){"use strict";function c(b,c){if(!a.isNumber(b)||a.isNaN(b)||!a.isFinite(b)||~~b!==b)throw new TypeError("`"+c+"` must be a finite integer");return b}function d(a){for(var b,c,d,e,f={},g=decodeURIComponent,h=a.split("&"),i=0,j=h.length;j>i;i++){var k=h[i];b=k.split("="),c=b[0],d=b[1]||!0,c=g(c),d=g(d),e=f[c],o(e)?e.push(d):f[c]=e?[e,d]:d}return f}function e(a,b,c){var d=a._events[b];if(d&&d.length){var e=d[d.length-1],f=e.callback;e.callback=function(){try{f.apply(this,arguments),c()}catch(a){throw a}finally{e.callback=f}}}else c()}var f=a.extend,g=a.omit,h=a.clone,i=a.each,j=a.pick,k=a.contains,l=a.isEmpty,m=a.pairs,n=a.invert,o=a.isArray,p=a.isFunction,q=a.isObject,r=a.keys,s=a.isUndefined,t=Math.ceil,u=Math.floor,v=Math.max,w=b.Collection.prototype,x=/[\s'"]/g,y=/[<>\s'"]/g,z=b.PageableCollection=b.Collection.extend({state:{firstPage:1,lastPage:null,currentPage:null,pageSize:25,totalPages:null,totalRecords:null,sortKey:null,order:-1},mode:"server",queryParams:{currentPage:"page",pageSize:"per_page",totalPages:"total_pages",totalRecords:"total_entries",sortKey:"sort_by",order:"order",directions:{"-1":"asc",1:"desc"}},constructor:function(a,b){w.constructor.apply(this,arguments),b=b||{};var c=this.mode=b.mode||this.mode||A.mode,d=f({},A.queryParams,this.queryParams,b.queryParams||{});d.directions=f({},A.queryParams.directions,this.queryParams.directions,d.directions||{}),this.queryParams=d;var e=this.state=f({},A.state,this.state,b.state||{});e.currentPage=null==e.currentPage?e.firstPage:e.currentPage,o(a)||(a=a?[a]:[]),a=a.slice(),"server"==c||null!=e.totalRecords||l(a)||(e.totalRecords=a.length),this.switchMode(c,f({fetch:!1,resetState:!1,models:a},b));var g=b.comparator;if(e.sortKey&&!g&&this.setSorting(e.sortKey,e.order,b),"server"!=c){var i=this.fullCollection;g&&b.full&&(this.comparator=null,i.comparator=g),b.full&&i.sort(),a&&!l(a)&&(this.reset(a,f({silent:!0},b)),this.getPage(e.currentPage),a.splice.apply(a,[0,a.length].concat(this.models)))}this._initState=h(this.state)},_makeFullCollection:function(a,c){var d,e,f,g=["url","model","sync","comparator"],h=this.constructor.prototype,i={};for(d=0,e=g.length;e>d;d++)f=g[d],s(h[f])||(i[f]=h[f]);var j=new(b.Collection.extend(i))(a,c);for(d=0,e=g.length;e>d;d++)f=g[d],this[f]!==h[f]&&(j[f]=this[f]);return j},_makeCollectionEventHandler:function(a,b){return function(c,d,g,j){var k=a._handlers;i(r(k),function(c){var d=k[c];a.off(c,d),b.off(c,d)});var l=h(a.state),m=l.firstPage,n=0===m?l.currentPage:l.currentPage-1,o=l.pageSize,p=n*o,q=p+o;if("add"==c){var u,v,w,x,j=j||{};if(g==b)v=b.indexOf(d),v>=p&&q>v&&(x=a,u=w=v-p);else{u=a.indexOf(d),v=p+u,x=b;var w=s(j.at)?v:j.at+p}if(j.onRemove||(++l.totalRecords,delete j.onRemove),a.state=a._checkState(l),x){x.add(d,f({},j||{},{at:w}));var y=u>=o?d:!s(j.at)&&q>w&&a.length>o?a.at(o):null;y&&e(g,c,function(){a.remove(y,{onAdd:!0})})}}if("remove"==c)if(j.onAdd)delete j.onAdd;else{if(--l.totalRecords){var z=l.totalPages=t(l.totalRecords/o);l.lastPage=0===m?z-1:z||m,l.currentPage>z&&(l.currentPage=l.lastPage)}else l.totalRecords=null,l.totalPages=null;a.state=a._checkState(l);var A,B=j.index;g==a?((A=b.at(q))?e(a,c,function(){a.push(A,{onRemove:!0})}):!a.length&&l.totalRecords&&a.reset(b.models.slice(p-o,q-o),f({},j,{parse:!1})),b.remove(d)):B>=p&&q>B&&((A=b.at(q-1))&&e(a,c,function(){a.push(A,{onRemove:!0})}),a.remove(d),!a.length&&l.totalRecords&&a.reset(b.models.slice(p-o,q-o),f({},j,{parse:!1})))}if("reset"==c)if(j=g,g=d,g==a&&null==j.from&&null==j.to){var C=b.models.slice(0,p),D=b.models.slice(p+a.models.length);b.reset(C.concat(a.models).concat(D),j)}else g==b&&((l.totalRecords=b.models.length)||(l.totalRecords=null,l.totalPages=null),"client"==a.mode&&(l.lastPage=l.currentPage=l.firstPage),a.state=a._checkState(l),a.reset(b.models.slice(p,q),f({},j,{parse:!1})));"sort"==c&&(j=g,g=d,g===b&&a.reset(b.models.slice(p,q),f({},j,{parse:!1}))),i(r(k),function(c){var d=k[c];i([a,b],function(a){a.on(c,d);var b=a._events[c]||[];b.unshift(b.pop())})})}},_checkState:function(a){var b=this.mode,d=this.links,e=a.totalRecords,f=a.pageSize,g=a.currentPage,h=a.firstPage,i=a.totalPages;if(null!=e&&null!=f&&null!=g&&null!=h&&("infinite"==b?d:!0)){if(e=c(e,"totalRecords"),f=c(f,"pageSize"),g=c(g,"currentPage"),h=c(h,"firstPage"),1>f)throw new RangeError("`pageSize` must be >= 1");if(i=a.totalPages=t(e/f),0>h||h>1)throw new RangeError("`firstPage must be 0 or 1`");if(a.lastPage=0===h?v(0,i-1):i||h,"infinite"==b){if(!d[g+""])throw new RangeError("No link found for page "+g)}else if(h>g||i>0&&(h?g>i:g>=i))throw new RangeError("`currentPage` must be firstPage <= currentPage "+(h?">":">=")+" totalPages if "+h+"-based. Got "+g+".")}return a},setPageSize:function(a,b){a=c(a,"pageSize"),b=b||{first:!1};var d=this.state,e=t(d.totalRecords/a),h=e?v(d.firstPage,u(e*d.currentPage/d.totalPages)):d.firstPage;return d=this.state=this._checkState(f({},d,{pageSize:a,currentPage:b.first?d.firstPage:h,totalPages:e})),this.getPage(d.currentPage,g(b,["first"]))},switchMode:function(b,c){if(!k(["server","client","infinite"],b))throw new TypeError('`mode` must be one of "server", "client" or "infinite"');c=c||{fetch:!0,resetState:!0};var d=this.state=c.resetState?h(this._initState):this._checkState(f({},this.state));this.mode=b;var e,j=this,l=this.fullCollection,m=this._handlers=this._handlers||{};if("server"==b||l)"server"==b&&l&&(i(r(m),function(a){e=m[a],j.off(a,e),l.off(a,e)}),delete this._handlers,this._fullComparator=l.comparator,delete this.fullCollection);else{l=this._makeFullCollection(c.models||[],c),l.pageableCollection=this,this.fullCollection=l;var n=this._makeCollectionEventHandler(this,l);i(["add","remove","reset","sort"],function(b){m[b]=e=a.bind(n,{},b),j.on(b,e),l.on(b,e)}),l.comparator=this._fullComparator}if("infinite"==b)for(var o=this.links={},p=d.firstPage,q=t(d.totalRecords/d.pageSize),s=0===p?v(0,q-1):q||p,u=d.firstPage;s>=u;u++)o[u]=this.url;else this.links&&delete this.links;return c.fetch?this.fetch(g(c,"fetch","resetState")):this},hasPreviousPage:function(){var a=this.state,b=a.currentPage;return"infinite"!=this.mode?b>a.firstPage:!!this.links[b-1]},hasNextPage:function(){var a=this.state,b=this.state.currentPage;return"infinite"!=this.mode?b<a.lastPage:!!this.links[b+1]},getFirstPage:function(a){return this.getPage("first",a)},getPreviousPage:function(a){return this.getPage("prev",a)},getNextPage:function(a){return this.getPage("next",a)},getLastPage:function(a){return this.getPage("last",a)},getPage:function(a,b){var d=this.mode,e=this.fullCollection;b=b||{fetch:!1};var h=this.state,i=h.firstPage,j=h.currentPage,k=h.lastPage,m=h.pageSize,n=a;switch(a){case"first":n=i;break;case"prev":n=j-1;break;case"next":n=j+1;break;case"last":n=k;break;default:n=c(a,"index")}this.state=this._checkState(f({},h,{currentPage:n})),b.from=j,b.to=n;var o=(0===i?n:n-1)*m,p=e&&e.length?e.models.slice(o,o+m):[];return"client"!=d&&("infinite"!=d||l(p))||b.fetch?("infinite"==d&&(b.url=this.links[n]),this.fetch(g(b,"fetch"))):(this.reset(p,g(b,"fetch")),this)},getPageByOffset:function(a,b){if(0>a)throw new RangeError("`offset must be > 0`");a=c(a);var d=u(a/this.state.pageSize);return 0!==this.state.firstPage&&d++,d>this.state.lastPage&&(d=this.state.lastPage),this.getPage(d,b)},sync:function(a,c,d){var e=this;if("infinite"==e.mode){var g=d.success,h=e.state.currentPage;d.success=function(a,b,c){var i=e.links,j=e.parseLinks(a,f({xhr:c},d));j.first&&(i[e.state.firstPage]=j.first),j.prev&&(i[h-1]=j.prev),j.next&&(i[h+1]=j.next),g&&g(a,b,c)}}return(w.sync||b.sync).call(e,a,c,d)},parseLinks:function(a,b){var c={},d=b.xhr.getResponseHeader("Link");if(d){var e=["first","prev","next"];i(d.split(","),function(a){var b=a.split(";"),d=b[0].replace(y,""),f=b.slice(1);i(f,function(a){var b=a.split("="),f=b[0].replace(x,""),g=b[1].replace(x,"");"rel"==f&&k(e,g)&&(c[g]=d)})})}return c},parse:function(a,b){var c=this.parseState(a,h(this.queryParams),h(this.state),b);return c&&(this.state=this._checkState(f({},this.state,c))),this.parseRecords(a,b)},parseState:function(b,c,d){if(b&&2===b.length&&q(b[0])&&o(b[1])){var e=h(d),f=b[0];return i(m(g(c,"directions")),function(b){var c=b[0],d=b[1],g=f[d];s(g)||a.isNull(g)||(e[c]=f[d])}),f.order&&(e.order=1*n(c.directions)[f.order]),e}},parseRecords:function(a){return a&&2===a.length&&q(a[0])&&o(a[1])?a[1]:a},fetch:function(a){a=a||{};var b=this._checkState(this.state),c=this.mode;"infinite"!=c||a.url||(a.url=this.links[b.currentPage]);var e=a.data||{},i=a.url||this.url||"";p(i)&&(i=i.call(this));var k=i.indexOf("?");-1!=k&&(f(e,d(i.slice(k+1))),i=i.slice(0,k)),a.url=i,a.data=e;var l,n,o,q,t="client"==this.mode?j(this.queryParams,"sortKey","order"):g(j(this.queryParams,r(A.queryParams)),"directions"),u=m(t),v=h(this);for(l=0;l<u.length;l++)n=u[l],o=n[0],q=n[1],q=p(q)?q.call(v):q,null!=b[o]&&null!=q&&(e[q]=b[o]);if(b.sortKey&&b.order){var x=p(t.order)?t.order.call(v):t.order;e[x]=this.queryParams.directions[b.order+""]}else b.sortKey||delete e[t.order];var y=m(g(this.queryParams,r(A.queryParams)));for(l=0;l<y.length;l++)n=y[l],q=n[1],q=p(q)?q.call(v):q,null!=q&&(e[n[0]]=q);if("server"!=c){var z=this,B=this.fullCollection,C=a.success;return a.success=function(b,d,e){e=e||{},s(a.silent)?delete e.silent:e.silent=a.silent;var g=b.models;"client"==c?B.reset(g,e):(B.add(g,f({at:B.length},f(e,{parse:!1}))),z.trigger("reset",z,e)),C&&C(b,d,e)},w.fetch.call(this,f({},a,{silent:!0}))}return w.fetch.call(this,a)},_makeComparator:function(a,b,c){var d=this.state;return a=a||d.sortKey,b=b||d.order,a&&b?(c||(c=function(a,b){return a.get(b)}),function(d,e){var f,g=c(d,a),h=c(e,a);return 1===b&&(f=g,g=h,h=f),g===h?0:h>g?-1:1}):void 0},setSorting:function(a,b,c){var d=this.state;d.sortKey=a,d.order=b=b||d.order;var e=this.fullCollection,g=!1,h=!1;a||(g=h=!0);var i=this.mode;c=f({side:"client"==i?i:"server",full:!0},c);var j=this._makeComparator(a,b,c.sortValue),k=c.full,l=c.side;return"client"==l?k?(e&&(e.comparator=j),g=!0):(this.comparator=j,h=!0):"server"!=l||k||(this.comparator=j),g&&(this.comparator=null),h&&e&&(e.comparator=null),this}}),A=z.prototype;return z});
\ No newline at end of file
diff --git a/htrace-core/src/web/lib/js/backgrid.min.js b/htrace-core/src/web/lib/js/backgrid-0.3.5.min.js
similarity index 100%
rename from htrace-core/src/web/lib/js/backgrid.min.js
rename to htrace-core/src/web/lib/js/backgrid-0.3.5.min.js
diff --git a/htrace-core/src/web/lib/js/backgrid-paginator.js b/htrace-core/src/web/lib/js/backgrid-paginator.js
new file mode 100644
index 0000000..64dd434
--- /dev/null
+++ b/htrace-core/src/web/lib/js/backgrid-paginator.js
@@ -0,0 +1,433 @@
+/*
+  backgrid-paginator
+  http://github.com/wyuenho/backgrid
+
+  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+  Licensed under the MIT @license.
+*/
+(function (root, factory) {
+
+  // CommonJS
+  if (typeof exports == "object") {
+    module.exports = factory(require("underscore"),
+                             require("backbone"),
+                             require("backgrid"),
+                             require("backbone.paginator"));
+  }
+  // AMD. Register as an anonymous module.
+  else if (typeof define === 'function' && define.amd) {
+    define(['underscore', 'backbone', 'backgrid', 'backbone.paginator'], factory);
+  }
+  // Browser
+  else {
+    factory(root._, root.Backbone, root.Backgrid);
+  }
+
+}(this, function (_, Backbone, Backgrid) {
+
+  "use strict";
+
+  /**
+     PageHandle is a class that renders the actual page handles and reacts to
+     click events for pagination.
+
+     This class acts in two modes - control or discrete page handle modes. If
+     one of the `is*` flags is `true`, an instance of this class is under
+     control page handle mode. Setting a `pageIndex` to an instance of this
+     class under control mode has no effect and the correct page index will
+     always be inferred from the `is*` flag. Only one of the `is*` flags should
+     be set to `true` at a time. For example, an instance of this class cannot
+     simultaneously be a rewind control and a fast forward control. A `label`
+     and a `title` function or a string are required to be passed to the
+     constuctor under this mode. If a `title` function is provided, it __MUST__
+     accept a hash parameter `data`, which contains a key `label`. Its result
+     will be used to render the generated anchor's title attribute.
+
+     If all of the `is*` flags is set to `false`, which is the default, an
+     instance of this class will be in discrete page handle mode. An instance
+     under this mode requires the `pageIndex` to be passed from the constructor
+     as an option and it __MUST__ be a 0-based index of the list of page numbers
+     to render. The constuctor will normalize the base to the same base the
+     underlying PageableCollection collection instance uses. A `label` is not
+     required under this mode, which will default to the equivalent 1-based page
+     index calculated from `pageIndex` and the underlying PageableCollection
+     instance. A provided `label` will still be honored however. The `title`
+     parameter is also not required under this mode, in which case the default
+     `title` function will be used. You are encouraged to provide your own
+     `title` function however if you wish to localize the title strings.
+
+     If this page handle represents the current page, an `active` class will be
+     placed on the root list element.
+
+     If this page handle is at the border of the list of pages, a `disabled`
+     class will be placed on the root list element.
+
+     Only page handles that are neither `active` nor `disabled` will respond to
+     click events and triggers pagination.
+
+     @class Backgrid.Extension.PageHandle
+  */
+  var PageHandle = Backgrid.Extension.PageHandle = Backbone.View.extend({
+
+    /** @property */
+    tagName: "li",
+
+    /** @property */
+    events: {
+      "click a": "changePage"
+    },
+
+    /**
+       @property {string|function(Object.<string, string>): string} title
+       The title to use for the `title` attribute of the generated page handle
+       anchor elements. It can be a string or a function that takes a `data`
+       parameter, which contains a mandatory `label` key which provides the
+       label value to be displayed.
+    */
+    title: function (data) {
+      return 'Page ' + data.label;
+    },
+
+    /**
+       @property {boolean} isRewind Whether this handle represents a rewind
+       control
+    */
+    isRewind: false,
+
+    /**
+       @property {boolean} isBack Whether this handle represents a back
+       control
+    */
+    isBack: false,
+
+    /**
+       @property {boolean} isForward Whether this handle represents a forward
+       control
+    */
+    isForward: false,
+
+    /**
+       @property {boolean} isFastForward Whether this handle represents a fast
+       forward control
+    */
+    isFastForward: false,
+
+    /**
+       Initializer.
+
+       @param {Object} options
+       @param {Backbone.Collection} options.collection
+       @param {number} pageIndex 0-based index of the page number this handle
+       handles. This parameter will be normalized to the base the underlying
+       PageableCollection uses.
+       @param {string} [options.label] If provided it is used to render the
+       anchor text, otherwise the normalized pageIndex will be used
+       instead. Required if any of the `is*` flags is set to `true`.
+       @param {string} [options.title]
+       @param {boolean} [options.isRewind=false]
+       @param {boolean} [options.isBack=false]
+       @param {boolean} [options.isForward=false]
+       @param {boolean} [options.isFastForward=false]
+    */
+    initialize: function (options) {
+      var collection = this.collection;
+      var state = collection.state;
+      var currentPage = state.currentPage;
+      var firstPage = state.firstPage;
+      var lastPage = state.lastPage;
+
+      _.extend(this, _.pick(options,
+                            ["isRewind", "isBack", "isForward", "isFastForward"]));
+
+      var pageIndex;
+      if (this.isRewind) pageIndex = firstPage;
+      else if (this.isBack) pageIndex = Math.max(firstPage, currentPage - 1);
+      else if (this.isForward) pageIndex = Math.min(lastPage, currentPage + 1);
+      else if (this.isFastForward) pageIndex = lastPage;
+      else {
+        pageIndex = +options.pageIndex;
+        pageIndex = (firstPage ? pageIndex + 1 : pageIndex);
+      }
+      this.pageIndex = pageIndex;
+
+      this.label = (options.label || (firstPage ? pageIndex : pageIndex + 1)) + '';
+      var title = options.title || this.title;
+      this.title = _.isFunction(title) ? title({label: this.label}) : title;
+    },
+
+    /**
+       Renders a clickable anchor element under a list item.
+    */
+    render: function () {
+      this.$el.empty();
+      var anchor = document.createElement("a");
+      anchor.href = '#';
+      if (this.title) anchor.title = this.title;
+      anchor.innerHTML = this.label;
+      this.el.appendChild(anchor);
+
+      var collection = this.collection;
+      var state = collection.state;
+      var currentPage = state.currentPage;
+      var pageIndex = this.pageIndex;
+
+      if (this.isRewind && currentPage == state.firstPage ||
+         this.isBack && !collection.hasPreviousPage() ||
+         this.isForward && !collection.hasNextPage() ||
+         this.isFastForward && (currentPage == state.lastPage || state.totalPages < 1)) {
+        this.$el.addClass("disabled");
+      }
+      else if (!(this.isRewind ||
+                 this.isBack ||
+                 this.isForward ||
+                 this.isFastForward) &&
+               state.currentPage == pageIndex) {
+        this.$el.addClass("active");
+      }
+
+      this.delegateEvents();
+      return this;
+    },
+
+    /**
+       jQuery click event handler. Goes to the page this PageHandle instance
+       represents. No-op if this page handle is currently active or disabled.
+    */
+    changePage: function (e) {
+      e.preventDefault();
+      var $el = this.$el, col = this.collection;
+      if (!$el.hasClass("active") && !$el.hasClass("disabled")) {
+        if (this.isRewind) col.getFirstPage();
+        else if (this.isBack) col.getPreviousPage();
+        else if (this.isForward) col.getNextPage();
+        else if (this.isFastForward) col.getLastPage();
+        else col.getPage(this.pageIndex, {reset: true});
+      }
+      return this;
+    }
+
+  });
+
+  /**
+     Paginator is a Backgrid extension that renders a series of configurable
+     pagination handles. This extension is best used for splitting a large data
+     set across multiple pages. If the number of pages is larger then a
+     threshold, which is set to 10 by default, the page handles are rendered
+     within a sliding window, plus the rewind, back, forward and fast forward
+     control handles. The individual control handles can be turned off.
+
+     @class Backgrid.Extension.Paginator
+  */
+  var Paginator = Backgrid.Extension.Paginator = Backbone.View.extend({
+
+    /** @property */
+    className: "backgrid-paginator",
+
+    /** @property */
+    windowSize: 10,
+
+    /**
+       @property {number} slideScale the number used by #slideHowMuch to scale
+       `windowSize` to yield the number of pages to slide. For example, the
+       default windowSize(10) * slideScale(0.5) yields 5, which means the window
+       will slide forward 5 pages as soon as you've reached page 6. The smaller
+       the scale factor the less pages to slide, and vice versa.
+
+       Also See:
+
+       - #slideMaybe
+       - #slideHowMuch
+    */
+    slideScale: 0.5,
+
+    /**
+       @property {Object.<string, Object.<string, string>>} controls You can
+       disable specific control handles by setting the keys in question to
+       null. The defaults will be merged with your controls object, with your
+       changes taking precedent.
+    */
+    controls: {
+      rewind: {
+        label: "《",
+        title: "First"
+      },
+      back: {
+        label: "〈",
+        title: "Previous"
+      },
+      forward: {
+        label: "〉",
+        title: "Next"
+      },
+      fastForward: {
+        label: "》",
+        title: "Last"
+      }
+    },
+
+    /** @property */
+    renderIndexedPageHandles: true,
+
+    /**
+       @property {Backgrid.Extension.PageHandle} pageHandle. The PageHandle
+       class to use for rendering individual handles
+    */
+    pageHandle: PageHandle,
+
+    /** @property */
+    goBackFirstOnSort: true,
+
+    /**
+       Initializer.
+
+       @param {Object} options
+       @param {Backbone.Collection} options.collection
+       @param {boolean} [options.controls]
+       @param {boolean} [options.pageHandle=Backgrid.Extension.PageHandle]
+       @param {boolean} [options.goBackFirstOnSort=true]
+    */
+    initialize: function (options) {
+      var self = this;
+      self.controls = _.defaults(options.controls || {}, self.controls,
+                                 Paginator.prototype.controls);
+
+      _.extend(self, _.pick(options || {}, "windowSize", "pageHandle",
+                            "slideScale", "goBackFirstOnSort",
+                            "renderIndexedPageHandles"));
+
+      var col = self.collection;
+      self.listenTo(col, "add", self.render);
+      self.listenTo(col, "remove", self.render);
+      self.listenTo(col, "reset", self.render);
+      self.listenTo(col, "backgrid:sorted", function () {
+        if (self.goBackFirstOnSort) col.getFirstPage({reset: true});
+      });
+    },
+
+    /**
+      Decides whether the window should slide. This method should return 1 if
+      sliding should occur and 0 otherwise. The default is sliding should occur
+      if half of the pages in a window has been reached.
+
+      __Note__: All the parameters have been normalized to be 0-based.
+
+      @param {number} firstPage
+      @param {number} lastPage
+      @param {number} currentPage
+      @param {number} windowSize
+      @param {number} slideScale
+
+      @return {0|1}
+     */
+    slideMaybe: function (firstPage, lastPage, currentPage, windowSize, slideScale) {
+      return Math.round(currentPage % windowSize / windowSize);
+    },
+
+    /**
+      Decides how many pages to slide when sliding should occur. The default
+      simply scales the `windowSize` to arrive at a fraction of the `windowSize`
+      to increment.
+
+      __Note__: All the parameters have been normalized to be 0-based.
+
+      @param {number} firstPage
+      @param {number} lastPage
+      @param {number} currentPage
+      @param {number} windowSize
+      @param {number} slideScale
+
+      @return {number}
+     */
+    slideThisMuch: function (firstPage, lastPage, currentPage, windowSize, slideScale) {
+      return ~~(windowSize * slideScale);
+    },
+
+    _calculateWindow: function () {
+      var collection = this.collection;
+      var state = collection.state;
+
+      // convert all indices to 0-based here
+      var firstPage = state.firstPage;
+      var lastPage = +state.lastPage;
+      lastPage = Math.max(0, firstPage ? lastPage - 1 : lastPage);
+      var currentPage = Math.max(state.currentPage, state.firstPage);
+      currentPage = firstPage ? currentPage - 1 : currentPage;
+      var windowSize = this.windowSize;
+      var slideScale = this.slideScale;
+      var windowStart = Math.floor(currentPage / windowSize) * windowSize;
+      if (currentPage <= lastPage - this.slideThisMuch()) {
+        windowStart += (this.slideMaybe(firstPage, lastPage, currentPage, windowSize, slideScale) *
+                        this.slideThisMuch(firstPage, lastPage, currentPage, windowSize, slideScale));
+      }
+      var windowEnd = Math.min(lastPage + 1, windowStart + windowSize);
+      return [windowStart, windowEnd];
+    },
+
+    /**
+       Creates a list of page handle objects for rendering.
+
+       @return {Array.<Object>} an array of page handle objects hashes
+    */
+    makeHandles: function () {
+
+      var handles = [];
+      var collection = this.collection;
+
+      var window = this._calculateWindow();
+      var winStart = window[0], winEnd = window[1];
+
+      if (this.renderIndexedPageHandles) {
+        for (var i = winStart; i < winEnd; i++) {
+          handles.push(new this.pageHandle({
+            collection: collection,
+            pageIndex: i
+          }));
+        }
+      }
+
+      var controls = this.controls;
+      _.each(["back", "rewind", "forward", "fastForward"], function (key) {
+        var value = controls[key];
+        if (value) {
+          var handleCtorOpts = {
+            collection: collection,
+            title: value.title,
+            label: value.label
+          };
+          handleCtorOpts["is" + key.slice(0, 1).toUpperCase() + key.slice(1)] = true;
+          var handle = new this.pageHandle(handleCtorOpts);
+          if (key == "rewind" || key == "back") handles.unshift(handle);
+          else handles.push(handle);
+        }
+      }, this);
+
+      return handles;
+    },
+
+    /**
+       Render the paginator handles inside an unordered list.
+    */
+    render: function () {
+      this.$el.empty();
+
+      if (this.handles) {
+        for (var i = 0, l = this.handles.length; i < l; i++) {
+          this.handles[i].remove();
+        }
+      }
+
+      var handles = this.handles = this.makeHandles();
+
+      var ul = document.createElement("ul");
+      for (var i = 0; i < handles.length; i++) {
+        ul.appendChild(handles[i].render().el);
+      }
+
+      this.el.appendChild(ul);
+
+      return this;
+    }
+
+  });
+
+}));
\ No newline at end of file
diff --git a/htrace-core/src/web/lib/js/backgrid-paginator.min.js b/htrace-core/src/web/lib/js/backgrid-paginator.min.js
new file mode 100644
index 0000000..5d3bc0b
--- /dev/null
+++ b/htrace-core/src/web/lib/js/backgrid-paginator.min.js
@@ -0,0 +1,9 @@
+/*
+ backgrid-paginator
+ http://github.com/wyuenho/backgrid
+
+ Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+ Licensed under the MIT @license.
+ */
+// Trunk from Commit ID: 669bca9383f1cafa53dc0efefe96a83c5315684d.
+!function(a,b){"object"==typeof exports&&(module.exports=b(require("underscore"),require("backbone"),require("backgrid"),require("backbone.paginator"))),"function"==typeof define&&define.amd?define(["underscore","backbone","backgrid","backbone.paginator"],b):b(a._,a.Backbone,a.Backgrid)}(this,function(a,b,c){"use strict";var d=c.Extension.PageHandle=b.View.extend({tagName:"li",events:{"click a":"changePage"},title:function(a){return"Page "+a.label},isRewind:!1,isBack:!1,isForward:!1,isFastForward:!1,initialize:function(b){var c=this.collection,d=c.state,e=d.currentPage,f=d.firstPage,g=d.lastPage;a.extend(this,a.pick(b,["isRewind","isBack","isForward","isFastForward"]));var h;this.isRewind?h=f:this.isBack?h=Math.max(f,e-1):this.isForward?h=Math.min(g,e+1):this.isFastForward?h=g:(h=+b.pageIndex,h=f?h+1:h),this.pageIndex=h,this.label=(b.label||(f?h:h+1))+"";var i=b.title||this.title;this.title=a.isFunction(i)?i({label:this.label}):i},render:function(){this.$el.empty();var a=document.createElement("a");a.href="#",this.title&&(a.title=this.title),a.innerHTML=this.label,this.el.appendChild(a);var b=this.collection,c=b.state,d=c.currentPage,e=this.pageIndex;return this.isRewind&&d==c.firstPage||this.isBack&&!b.hasPreviousPage()||this.isForward&&!b.hasNextPage()||this.isFastForward&&(d==c.lastPage||c.totalPages<1)?this.$el.addClass("disabled"):this.isRewind||this.isBack||this.isForward||this.isFastForward||c.currentPage!=e||this.$el.addClass("active"),this.delegateEvents(),this},changePage:function(a){a.preventDefault();var b=this.$el,c=this.collection;return b.hasClass("active")||b.hasClass("disabled")||(this.isRewind?c.getFirstPage():this.isBack?c.getPreviousPage():this.isForward?c.getNextPage():this.isFastForward?c.getLastPage():c.getPage(this.pageIndex,{reset:!0})),this}}),e=c.Extension.Paginator=b.View.extend({className:"backgrid-paginator",windowSize:10,slideScale:.5,controls:{rewind:{label:"《",title:"First"},back:{label:"〈",title:"Previous"},forward:{label:"〉",title:"Next"},fastForward:{label:"》",title:"Last"}},renderIndexedPageHandles:!0,pageHandle:d,goBackFirstOnSort:!0,initialize:function(b){var c=this;c.controls=a.defaults(b.controls||{},c.controls,e.prototype.controls),a.extend(c,a.pick(b||{},"windowSize","pageHandle","slideScale","goBackFirstOnSort","renderIndexedPageHandles"));var d=c.collection;c.listenTo(d,"add",c.render),c.listenTo(d,"remove",c.render),c.listenTo(d,"reset",c.render),c.listenTo(d,"backgrid:sorted",function(){c.goBackFirstOnSort&&d.getFirstPage({reset:!0})})},slideMaybe:function(a,b,c,d){return Math.round(c%d/d)},slideThisMuch:function(a,b,c,d,e){return~~(d*e)},_calculateWindow:function(){var a=this.collection,b=a.state,c=b.firstPage,d=+b.lastPage;d=Math.max(0,c?d-1:d);var e=Math.max(b.currentPage,b.firstPage);e=c?e-1:e;var f=this.windowSize,g=this.slideScale,h=Math.floor(e/f)*f;e<=d-this.slideThisMuch()&&(h+=this.slideMaybe(c,d,e,f,g)*this.slideThisMuch(c,d,e,f,g));var i=Math.min(d+1,h+f);return[h,i]},makeHandles:function(){var b=[],c=this.collection,d=this._calculateWindow(),e=d[0],f=d[1];if(this.renderIndexedPageHandles)for(var g=e;f>g;g++)b.push(new this.pageHandle({collection:c,pageIndex:g}));var h=this.controls;return a.each(["back","rewind","forward","fastForward"],function(a){var d=h[a];if(d){var e={collection:c,title:d.title,label:d.label};e["is"+a.slice(0,1).toUpperCase()+a.slice(1)]=!0;var f=new this.pageHandle(e);"rewind"==a||"back"==a?b.unshift(f):b.push(f)}},this),b},render:function(){if(this.$el.empty(),this.handles)for(var a=0,b=this.handles.length;b>a;a++)this.handles[a].remove();for(var c=this.handles=this.makeHandles(),d=document.createElement("ul"),a=0;a<c.length;a++)d.appendChild(c[a].render().el);return this.el.appendChild(d),this}})});
\ No newline at end of file