You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@htrace.apache.org by iw...@apache.org on 2015/09/16 06:29:57 UTC

[11/22] incubator-htrace git commit: HTRACE-246. HTrace WebApp not properly defined and therefore not packaged into .war (Lewis John McGibbney via iwasakims)

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/05ce37fb/htrace-webapp/src/main/webapp/app/search_view.js
----------------------------------------------------------------------
diff --git a/htrace-webapp/src/main/webapp/app/search_view.js b/htrace-webapp/src/main/webapp/app/search_view.js
new file mode 100644
index 0000000..aeb4273
--- /dev/null
+++ b/htrace-webapp/src/main/webapp/app/search_view.js
@@ -0,0 +1,196 @@
+/*
+ * 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 htrace = htrace || {};
+htrace.SearchView = Backbone.View.extend({
+  initialize : function() {
+    this.predicateViews = [];
+    this.highestPredicateIndex = 0;
+    this.searchInProgress = false;
+    this.searchResults = new htrace.SearchResults();
+    this.resultsView = new htrace.SearchResultsView({
+        searchResults: this.searchResults,
+        el: "#resultsView"
+    });
+  },
+
+  events: {
+    "click #searchButton": "searchHandler",
+    "click #clearButton": "clearHandler",
+    "click .add-field": "dropdownHandler",
+    "blur #begin": "blurBeginHandler",
+    "blur #end": "blurEndHandler",
+    "click #zoomButton": "zoomHandler"
+  },
+
+  searchHandler: function(e){
+    e.preventDefault();
+
+    this.doSearch(e.ctrlKey);
+  },
+
+  clearHandler: function(e){
+    e.preventDefault();
+
+    this.resultsView.clearHandler(true);
+  },
+
+  doSearch: function(showDebug){
+    if (this.searchInProgress) {
+      console.log("Can't start a new search while another one is in " +
+          "progress.");
+      return false;
+    }
+
+    // Check if there are no search criteria.
+    if (this.predicateViews.length == 0) {
+      htrace.showModalWarning("No Search Criteria Specified",
+        "You have not specified any search criteria.  " +
+        "Use the 'Add Predicate' button to specify what to search for.");
+      return false;
+    }
+
+    // Build the predicate array.
+    predicates = []
+    var predicateViewsLen = this.predicateViews.length;
+    for (var i = 0; i < predicateViewsLen; i++) {
+      var predicateView = this.predicateViews[i];
+      try {
+        predicates.push(predicateView.getPredicate());
+      } catch(err) {
+        htrace.showModalWarning("Search Field Validation Error",
+          "Invalid search string for the '" + predicateView.ptype.name +
+          "' field.<p/>" + err);
+        return false;
+      }
+    }
+    var queryJson = {
+      pred: predicates,
+      lim: 20
+    };
+    // If there are existing search results, we want results which "come after"
+    // those.  So pass the last span we saw as a continuation token.
+    if (this.searchResults.size() > 0) {
+      queryJson.prev =
+          this.searchResults.at(this.searchResults.size() - 1).unparse();
+    }
+    var searchView = this;
+    var queryResults = new htrace.QueryResults({queryJson: queryJson});
+    console.log("Starting span query " + queryResults.url());
+    this.searchInProgress = true;
+    queryResults.fetch({
+      success: function(model, response, options){
+        var firstResults = (searchView.searchResults.size() === 0);
+        console.log("Success on span query " + queryResults.url() + ": got " +
+            queryResults.size() + " result(s).  firstResults=" + firstResults);
+        searchView.searchResults.add(queryResults.models);
+        if (firstResults) {
+          // After the initial search, zoom to fit everything.
+          // On subsequent searches, we leave the viewport alone.
+          searchView.resultsView.zoomHandler();
+        }
+        searchView.searchInProgress = false;
+        if (showDebug) {
+          htrace.showModalWarning("Search Debug",
+            "This is the search debug box, accessible by holding down the " +
+            "control key while clicking the search button.<p/>" +
+            "<h3>Query JSON</h3><pre>" + queryResults.prettyQueryString() +
+            "</pre><p/><h3>Response JSON</h3><pre>" +
+            JSON.stringify(queryResults, null, 2) + "</pre><p/>");
+        } else if (queryResults.size() == 0) {
+          if (firstResults) {
+            htrace.showModalWarning("No Results Found",
+              "No results were found for your query.<p/>");
+          } else {
+            htrace.showModalWarning("No Additional Results Found",
+              "No additional results were found for your query.<p/>");
+          }
+        }
+        searchView.resultsView.render();
+      },
+      error: function(model, response, options){
+        searchView.searchResults.reset();
+        var err = "Error " + JSON.stringify(response, null, 2) +
+          " on span query " + queryResults.url();
+        console.log(err);
+        alert(err);
+        searchView.searchInProgress = false;
+      }
+    });
+    return false;
+  },
+
+  dropdownHandler: function(e){
+    e.preventDefault();
+    var text = $(e.target).text();
+    var ptype = htrace.parsePType(text);
+    if (!ptype) {
+      alert("Unable to parse predicate type '" + text + "'");
+      return false;
+    }
+    var index = this.highestPredicateIndex;
+    this.highestPredicateIndex++;
+    var el = "pred" + index;
+    $("#predicates").append('<div id="' + el + '"/></div>');
+    predicateView = new htrace.PredicateView({
+      el: "#" + el,
+      index: index,
+      ptype: ptype,
+      searchView: this
+    });
+    this.predicateViews.push(predicateView);
+    predicateView.render();
+    return true;
+  },
+
+  blurBeginHandler: function(e) {
+    return this.resultsView.handleBeginOrEndChange(e, "begin");
+  },
+
+  blurEndHandler: function(e) {
+    return this.resultsView.handleBeginOrEndChange(e, "end");
+  },
+
+  zoomHandler: function(e) {
+    e.preventDefault();
+    this.resultsView.zoomHandler();
+  },
+
+  removePredicateView: function(predicateView) {
+    this.predicateViews = _.without(this.predicateViews, predicateView);
+  },
+
+  render: function() {
+    this.$el.html(_.template($("#search-view-template").html())
+      ({ model : this.model }))
+    this.resultsView.render();
+    console.log("SearchView#render");
+    return this;
+  },
+
+  close: function() {
+    console.log("SearchView#close")
+    while (this.predicateViews.length > 0) {
+      this.predicateViews[0].remove();
+    }
+    this.resultsView.remove();
+    this.resultsView = null;
+    this.undelegateEvents();
+  }
+});

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/05ce37fb/htrace-webapp/src/main/webapp/app/server_info.js
----------------------------------------------------------------------
diff --git a/htrace-webapp/src/main/webapp/app/server_info.js b/htrace-webapp/src/main/webapp/app/server_info.js
new file mode 100644
index 0000000..b03f706
--- /dev/null
+++ b/htrace-webapp/src/main/webapp/app/server_info.js
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+// htraced ServerInfo sent back from /serverInfo.
+// See rest.go.
+htrace.ServerInfo = Backbone.Model.extend({
+  defaults: {
+    "ReleaseVersion": "unknown",
+    "GitVersion": "unknown",
+  },
+
+  url: function() {
+    return "server/info";
+  }
+});

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/05ce37fb/htrace-webapp/src/main/webapp/app/span.js
----------------------------------------------------------------------
diff --git a/htrace-webapp/src/main/webapp/app/span.js b/htrace-webapp/src/main/webapp/app/span.js
new file mode 100644
index 0000000..cd87543
--- /dev/null
+++ b/htrace-webapp/src/main/webapp/app/span.js
@@ -0,0 +1,282 @@
+/*
+ * 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 htrace = htrace || {};
+
+// The invalid span ID, which is all zeroes.
+htrace.INVALID_SPAN_ID = "00000000000000000000000000000000";
+
+// Convert an array of htrace.Span models into a comma-separated string.
+htrace.spanModelsToString = function(spans) {
+  var ret = "";
+  var prefix = "";
+  for (var i = 0; i < spans.length; i++) {
+    ret += prefix + JSON.stringify(spans[i].unparse());
+    prefix = ", ";
+  }
+  return ret;
+};
+
+// Convert an array of return results from ajax calls into an array of
+// htrace.Span models.
+htrace.parseMultiSpanAjaxQueryResults = function(ajaxCalls) {
+  var parsedSpans = [];
+  for (var i = 0; i < ajaxCalls.length; i++) {
+    var text = ajaxCalls[i][0];
+    var result = ajaxCalls[i][1];
+    if (ajaxCalls[i]["status"] != "200") {
+      throw "ajax error: " + ajaxCalls[i].statusText;
+    }
+    var parsedSpan = new htrace.Span({});
+    try {
+      parsedSpan.parse(ajaxCalls[i].responseJSON, {});
+    } catch (e) {
+      throw "span parse error: " + e;
+    }
+    parsedSpans.push(parsedSpan);
+  }
+  return parsedSpans;
+};
+
+htrace.sortSpansByBeginTime = function(spans) {
+  return spans.sort(function(a, b) {
+      if (a.get("begin") < b.get("begin")) {
+        return -1;
+      } else if (a.get("begin") > b.get("begin")) {
+        return 1;
+      } else {
+        return 0;
+      }
+    });
+};
+
+htrace.getReifiedParents = function(span) {
+  return span.get("reifiedParents") || [];
+};
+
+htrace.getReifiedChildren = function(span) {
+  return span.get("reifiedChildren") || [];
+};
+
+htrace.Span = Backbone.Model.extend({
+  // Parse a span sent from htraced.
+  // We use more verbose names for some attributes.
+  // Missing attributes are treated as zero or empty.  Numerical attributes are
+  // forced to be numbers.
+  parse: function(response, options) {
+    var span = {};
+    this.set("spanId", response.a ? response.a : htrace.INVALID_SPAN_ID);
+    this.set("tracerId", response.r ? response.r : "");
+    this.set("parents", response.p ? response.p : []);
+    this.set("description", response.d ? response.d : "");
+    this.set("begin", response.b ? parseInt(response.b, 10) : 0);
+    this.set("end", response.e ? parseInt(response.e, 10) : 0);
+    if (response.t) {
+      var t = response.t.sort(function(a, b) {
+          if (a.t < b.t) {
+            return -1;
+          } else if (a.t > b.t) {
+            return 1;
+          } else {
+            return 0;
+          }
+        });
+      this.set("timeAnnotations", t);
+    } else {
+      this.set("timeAnnotations", []);
+    }
+    this.set("infoAnnotations", response.n ? response.n : {});
+    this.set("selected", false);
+
+    // reifiedChildren starts off as null and will be filled in as needed.
+    this.set("reifiedChildren", null);
+
+    // If there are parents, reifiedParents starts off as null.  Otherwise, we
+    // know it is the empty array.
+    this.set("reifiedParents", (this.get("parents").length == 0) ? [] : null);
+
+    return span;
+  },
+
+  // Transform a span model back into a JSON string suitable for sending over
+  // the wire.
+  unparse: function() {
+    var obj = { };
+    if (!(this.get("spanId") === htrace.INVALID_SPAN_ID)) {
+      obj.a = this.get("spanId");
+    }
+    if (!(this.get("tracerId") === "")) {
+      obj.r = this.get("tracerId");
+    }
+    if (this.get("parents").length > 0) {
+      obj.p = this.get("parents");
+    }
+    if (this.get("description").length > 0) {
+      obj.d = this.get("description");
+    }
+    if (this.get("begin") > 0) {
+      obj.b = this.get("begin");
+    }
+    if (this.get("end") > 0) {
+      obj.e = this.get("end");
+    }
+    if (this.get("timeAnnotations").length > 0) {
+      obj.t = this.get("timeAnnotations");
+    }
+    if (_.size(this.get("infoAnnotations")) > 0) {
+      obj.n = this.get("infoAnnotations");
+    }
+    return obj;
+  },
+
+  //
+  // Although the parent IDs are always present in the 'parents' field of the
+  // span, sometimes we need the actual parent span models.  In that case we
+  // must "reify" them (make them real).
+  //
+  // This functionReturns a jquery promise which reifies all the parents of this
+  // span and stores them into reifiedParents.  The promise returns the empty
+  // string on success, or an error string on failure.
+  //
+  reifyParents: function() {
+    var span = this;
+    var numParents = span.get("parents").length;
+    var ajaxCalls = [];
+    // Set up AJAX queries to reify the parents.
+    for (var i = 0; i < numParents; i++) {
+      ajaxCalls.push($.ajax({
+        url: "span/" + span.get("parents")[i],
+        data: {},
+        contentType: "application/json; charset=utf-8",
+        dataType: "json"
+      }));
+    }
+    var rootDeferred = jQuery.Deferred();
+    $.when.apply($, ajaxCalls).then(function() {
+      var reifiedParents = [];
+      try {
+        reifiedParents = htrace.parseMultiSpanAjaxQueryResults(ajaxCalls);
+      } catch (e) {
+        rootDeferred.resolve("Error reifying parents for " +
+            span.get("spanId") + ": " + e);
+        return;
+      }
+      reifiedParents = htrace.sortSpansByBeginTime(reifiedParents);
+      // The current span is a child of the reified parents.  There may be other
+      // children of those parents, but we are ignoring that here.  By making
+      // this non-null, the "expand children" button will not appear for these
+      // paren spans.
+      for (var j = 0; j < reifiedParents.length; j++) {
+        reifiedParents[j].set("reifiedChildren", [span]);
+      }
+      console.log("Setting reified parents for " + span.get("spanId") +
+          " to " + htrace.spanModelsToString (reifiedParents));
+      span.set("reifiedParents", reifiedParents);
+      rootDeferred.resolve("");
+    });
+    return rootDeferred.promise();
+  },
+
+  //
+  // The span itself does not contain its children.  However, the server has an
+  // index which can be used to easily find the children of a particular span.
+  //
+  // This function returns a jquery promise which reifies all the children of
+  // this span and stores them into reifiedChildren.  The promise returns the
+  // empty string on success, or an error string on failure.
+  //
+  reifyChildren: function() {
+    var rootDeferred = jQuery.Deferred();
+    var span = this;
+    $.ajax({
+        url: "span/" + span.get("spanId") + "/children?lim=50",
+        data: {},
+        contentType: "application/json; charset=utf-8",
+        dataType: "json"
+      }).done(function(childIds) {
+        var ajaxCalls = [];
+        for (var i = 0; i < childIds.length; i++) {
+          ajaxCalls.push($.ajax({
+            url: "span/" + childIds[i],
+            data: {},
+            contentType: "application/json; charset=utf-8",
+            dataType: "json"
+          }));
+        };
+        $.when.apply($, ajaxCalls).then(function() {
+          var reifiedChildren;
+          try {
+            reifiedChildren = htrace.parseMultiSpanAjaxQueryResults(ajaxCalls);
+          } catch (e) {
+            reifiedChildren = rootDeferred.resolve("Error reifying children " +
+                "for " + span.get("spanId") + ": " + e);
+            return;
+          }
+          reifiedChildren = htrace.sortSpansByBeginTime(reifiedChildren);
+          // The current span is a parent of the new child.
+          // There may be other parents, but we are ignoring that here.
+          // By making this non-null, the "expand parents" button will not
+          // appear for these child spans.
+          for (var j = 0; j < reifiedChildren.length; j++) {
+            reifiedChildren[j].set("reifiedParents", [span]);
+          }
+          console.log("Setting reified children for " + span.get("spanId") +
+              " to " + htrace.spanModelsToString (reifiedChildren));
+          span.set("reifiedChildren", reifiedChildren);
+          rootDeferred.resolve("");
+        });
+      }).fail(function(statusData) {
+        // Check if the /children query failed.
+        rootDeferred.resolve("Error querying children of " +
+            span.get("spanId") + ": got " + statusData);
+        return;
+      });
+    return rootDeferred.promise();
+  },
+
+  // Get the earliest begin time of this span or any of its reified parents or
+  // children.
+  getEarliestBegin: function() {
+    var earliestBegin = this.get("begin");
+    htrace.treeTraverseDepthFirstPre(this, htrace.getReifiedParents, 0,
+        function(span, depth) {
+          earliestBegin = Math.min(earliestBegin, span.get("begin"));
+        });
+    htrace.treeTraverseDepthFirstPre(this, htrace.getReifiedChildren, 0,
+        function(span, depth) {
+          earliestBegin = Math.min(earliestBegin, span.get("begin"));
+        });
+    return earliestBegin;
+  },
+
+  // Get the earliest begin time of this span or any of its reified parents or
+  // children.
+  getLatestEnd: function() {
+    var latestEnd = this.get("end");
+    htrace.treeTraverseDepthFirstPre(this, htrace.getReifiedParents, 0,
+        function(span, depth) {
+          latestEnd = Math.max(latestEnd, span.get("end"));
+        });
+    htrace.treeTraverseDepthFirstPre(this, htrace.getReifiedChildren, 0,
+        function(span, depth) {
+          latestEnd = Math.max(latestEnd, span.get("end"));
+        });
+    return latestEnd;
+  },
+});

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/05ce37fb/htrace-webapp/src/main/webapp/app/span_details_view.js
----------------------------------------------------------------------
diff --git a/htrace-webapp/src/main/webapp/app/span_details_view.js b/htrace-webapp/src/main/webapp/app/span_details_view.js
new file mode 100644
index 0000000..9a37055
--- /dev/null
+++ b/htrace-webapp/src/main/webapp/app/span_details_view.js
@@ -0,0 +1,39 @@
+/*
+ * 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 htrace = htrace || {};
+
+htrace.SpanDetailsView = Backbone.View.extend({
+  initialize: function(options) {
+    this.el = options.el;
+    this.model = options.model;
+  }
+
+  render: function() {
+    this.$el.html(_.template($("#about-view-template").html())
+      ({ model : this.model }));
+    console.log("AboutView#render");
+    return this;
+  },
+
+  close: function() {
+    console.log("AboutView#close")
+    this.undelegateEvents();
+  }
+});

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/05ce37fb/htrace-webapp/src/main/webapp/app/span_group_widget.js
----------------------------------------------------------------------
diff --git a/htrace-webapp/src/main/webapp/app/span_group_widget.js b/htrace-webapp/src/main/webapp/app/span_group_widget.js
new file mode 100644
index 0000000..ad0b482
--- /dev/null
+++ b/htrace-webapp/src/main/webapp/app/span_group_widget.js
@@ -0,0 +1,103 @@
+/*
+ * 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 htrace = htrace || {};
+
+// Widget containing a group of trace spans displayed on the canvas.
+htrace.SpanGroupWidget = function(params) {
+  this.draw = function() {
+    this.ctx.save();
+    this.ctx.fillStyle="#ffffff";
+    this.ctx.fillRect(this.x0, this.y0, this.xF - this.x0, this.yF - this.y0);
+    this.ctx.strokeStyle="#aaaaaa";
+    this.ctx.beginPath();
+    this.ctx.moveTo(this.x0, this.y0);
+    this.ctx.lineTo(this.xF, this.y0);
+    this.ctx.stroke();
+    this.ctx.beginPath();
+    this.ctx.moveTo(this.x0, this.yF);
+    this.ctx.lineTo(this.xF, this.yF);
+    this.ctx.stroke();
+    this.ctx.restore();
+    return true;
+  };
+
+  this.createSpanWidget = function(node, indentLevel,
+      allowUpButton, allowDownButton) {
+    new htrace.SpanWidget({
+      manager: this.manager,
+      ctx: this.ctx,
+      span: node,
+      x0: this.x0,
+      xB: this.xB,
+      xD: this.xD,
+      xF: this.xF,
+      xT: this.childIndent * indentLevel,
+      y0: this.spanY,
+      yF: this.spanY + this.spanWidgetHeight,
+      allowUpButton: allowUpButton,
+      allowDownButton: allowDownButton,
+      begin: this.begin,
+      end: this.end
+    });
+    this.spanY += this.spanWidgetHeight;
+  }
+
+  this.handle = function(e) {
+    switch (e.type) {
+      case "draw":
+        this.draw();
+        return true;
+    }
+  }
+
+  for (var k in params) {
+    this[k]=params[k];
+  }
+  this.manager.register("draw", this);
+  this.spanY = this.y0 + 4;
+
+  // Figure out how much to indent each child's description text.
+  this.childIndent = Math.max(10, (this.xF - this.xD) / 50);
+
+  // Get the maximum depth of the parents tree to find out how far to indent.
+  var parentTreeHeight =
+      htrace.treeHeight(this.span, htrace.getReifiedParents);
+
+  // Traverse the parents tree upwards.
+  var thisWidget = this;
+  htrace.treeTraverseDepthFirstPost(this.span, htrace.getReifiedParents, 0,
+      function(node, depth) {
+        if (depth > 0) {
+          thisWidget.createSpanWidget(node,
+              parentTreeHeight - depth, true, false);
+        }
+      });
+  thisWidget.createSpanWidget(this.span, parentTreeHeight, true, true);
+  // Traverse the children tree downwards.
+  htrace.treeTraverseDepthFirstPre(this.span, htrace.getReifiedChildren, 0,
+      function(node, depth) {
+        if (depth > 0) {
+          thisWidget.createSpanWidget(node,
+              parentTreeHeight + depth, false, true);
+        }
+      });
+  this.yF = this.spanY + 4;
+  return this;
+};

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/05ce37fb/htrace-webapp/src/main/webapp/app/span_widget.js
----------------------------------------------------------------------
diff --git a/htrace-webapp/src/main/webapp/app/span_widget.js b/htrace-webapp/src/main/webapp/app/span_widget.js
new file mode 100644
index 0000000..50bea91
--- /dev/null
+++ b/htrace-webapp/src/main/webapp/app/span_widget.js
@@ -0,0 +1,309 @@
+/*
+ * 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 htrace = htrace || {};
+
+htrace.showSpanDetails = function(span) {
+  var info = {
+    spanID: span.get("spanID"),
+    begin: htrace.dateToString(span.get("begin"), 10),
+    end: htrace.dateToString(span.get("end"), 10),
+    duration: ((span.get("end") - span.get("begin")) + " ms")
+  };
+  var explicitOrder = {
+    spanId: 1,
+    begin: 2,
+    end: 3,
+    duration: 4
+  };
+  keys = ["duration"];
+  for(k in span.attributes) {
+    if (k == "reifiedChildren") {
+      continue;
+    }
+    if (k == "reifiedParents") {
+      continue;
+    }
+    if (k == "selected") {
+      continue;
+    }
+    if (k == "timeAnnotations") {
+      // For timeline annotations, make the times into top-level keys.
+      var timeAnnotations = span.get("timeAnnotations");
+      for (var i = 0; i < timeAnnotations.length; i++) {
+        var key = htrace.dateToString(timeAnnotations[i].t);
+        keys.push(key);
+        info[key] = timeAnnotations[i].m;
+        explicitOrder[key] = 200;
+      }
+      continue;
+    }
+    if (k == "infoAnnotations") {
+      // For info annotations, move the keys to the top level.
+      // Surround them in brackets to make it clear that they are
+      // user-defined.
+      var infoAnnotations = span.get("infoAnnotations");
+      _.each(infoAnnotations, function(value, key) {
+        key = "[" + key + "]";
+        keys.push(key);
+        info[key] = value;
+        explicitOrder[key] = 200;
+      });
+      continue;
+    }
+    keys.push(k);
+    if (info[k] == null) {
+      info[k] = span.get(k);
+    }
+  }
+  // We sort the keys so that the stuff we want at the top appears at the top,
+  // and everything else is in alphabetical order.
+  keys = keys.sort(function(a, b) {
+      var oa = explicitOrder[a] || 100;
+      var ob = explicitOrder[b] || 100;
+      if (oa < ob) {
+        return -1;
+      } else if (oa > ob) {
+        return 1;
+      } else if (a < b) {
+        return -1;
+      } else if (a > b) {
+        return 1;
+      } else {
+        return 0;
+      }
+    });
+  var len = keys.length;
+  var h = '<table style="table-layout:fixed;width:100%;word-wrap:break-word">';
+  for (i = 0; i < len; i++) {
+    // Make every other row grey to improve visibility.
+    var colorString = ((i%2) == 1) ? "#f1f1f1" : "#ffffff";
+    h += _.template($("#table-row-template").html())(
+        {bgcolor: colorString, key: keys[i], val: info[keys[i]]});
+  }
+  h += '</table>';
+  htrace.showModal(_.template($("#modal-table-template").html())(
+      {title: "Span Details", body: h}));
+};
+
+// Widget containing the trace span displayed on the canvas.
+htrace.SpanWidget = function(params) {
+  this.draw = function() {
+    this.drawBackground();
+    this.drawTracerId();
+    this.drawDescription();
+  };
+
+  // Draw the background of this span widget.
+  this.drawBackground = function() {
+    this.ctx.save();
+    if (this.span.get("selected")) {
+      this.ctx.fillStyle="#ffccff";
+    } else {
+      this.ctx.fillStyle="#ffffff";
+    }
+    this.ctx.fillRect(this.x0, this.y0, this.xSize, this.ySize);
+    this.ctx.restore();
+  }
+
+  // Draw process ID text.
+  this.drawTracerId = function() {
+    this.ctx.save();
+    this.ctx.fillStyle="#000000";
+    this.ctx.font = (this.ySize - 2) + "px sans-serif";
+    this.ctx.beginPath();
+    this.ctx.rect(this.x0, this.y0, this.xB - this.x0, this.ySize);
+    this.ctx.clip();
+    this.ctx.fillText(this.span.get('tracerId'), this.x0, this.yF - 4);
+    this.ctx.restore();
+  };
+
+  // Draw the span description
+  this.drawDescription = function() {
+    // Draw the light blue bar representing time.
+    this.ctx.save();
+    this.ctx.beginPath();
+    this.ctx.rect(this.xD, this.y0, this.xF - this.xD, this.ySize);
+    this.ctx.clip();
+    this.ctx.strokeStyle="#000000";
+    this.ctx.fillStyle="#a7b7ff";
+    var beginX = this.timeToPosition(this.span.get('begin'));
+    var endX = this.timeToPosition(this.span.get('end'));
+
+    // If the span is completely off the screen, draw a diamond at either the
+    // beginning or the end of the bar to indicate whether it's too early or too
+    // late to be seen.
+    if (endX < this.x0) {
+      beginX = this.xD;
+      endX = this.xD;
+    }
+    if (beginX > this.xF) {
+      beginX = this.xF;
+      endX = this.xF;
+    }
+
+    var gapY = 2;
+    var epsilon = Math.max(2, Math.floor(this.xSize / 1000));
+    if (endX - beginX < epsilon) {
+      // The time interval is too narrow to see.  Draw a diamond on the point instead.
+      this.ctx.beginPath();
+      this.ctx.moveTo(beginX, this.y0 + gapY);
+      this.ctx.lineTo(beginX + (Math.floor(this.ySize / 2) - gapY),
+          this.y0 + Math.floor(this.ySize / 2));
+      this.ctx.lineTo(beginX, this.yF - gapY);
+      this.ctx.lineTo(beginX - (Math.floor(this.ySize / 2) - gapY),
+          this.y0 + Math.floor(this.ySize / 2));
+      this.ctx.closePath();
+      this.ctx.fill();
+    } else {
+      // Draw a bar from the start time to the end time.
+//      console.log("beginX=" + beginX + ", endX=" + endX +
+//          ", begin=" + this.span.get('begin') + ", end=" + this.span.get('end'));
+      this.ctx.fillRect(beginX, this.y0 + gapY, endX - beginX,
+          this.ySize - (gapY * 2));
+
+      // Draw a dots showing time points where annotations are.
+      var annotations = this.span.get('timeAnnotations');
+      var annotationY = this.y0 + gapY;
+      var annotationW = 4;
+      var annotationH = (this.ySize - (gapY * 2)) / 2;
+      this.ctx.fillStyle="#419641";
+      for (var i = 0; i < annotations.length; i++) {
+        this.ctx.fillRect(this.timeToPosition(annotations[i].t), annotationY,
+            annotationW, annotationH);
+      }
+    }
+
+    // Draw description text
+    this.ctx.fillStyle="#000000";
+    this.ctx.font = (this.ySize - gapY) + "px sans-serif";
+    this.ctx.fillText(this.span.get('description'),
+        this.xD + this.xT,
+        this.yF - gapY - 2);
+
+    this.ctx.restore();
+  };
+
+  // Convert a time in milliseconds since the epoch to an x position.
+  this.timeToPosition = function(time) {
+    return this.xD +
+      (((time - this.begin) * (this.xF - this.xD)) /
+        (this.end - this.begin));
+  };
+
+  this.handle = function(e) {
+    switch (e.type) {
+      case "mouseDown":
+        if (!htrace.inBoundingBox(e.x, e.y,
+              this.x0, this.xF, this.y0, this.yF)) {
+          return true;
+        }
+        if (e.raw.ctrlKey) {
+          // If the control key is pressed, we toggle the current selection.
+          // The user can create multiple selections this way.
+          if (this.span.get("selected")) {
+            this.span.set("selected", false);
+          } else {
+            this.span.set("selected", true);
+          }
+        } else {
+          var that = this;
+          this.manager.searchResultsView.applyToAllSpans(function(span) {
+              // Note: we don't want to set the selection state unless we need
+              // to.  Setting the state (even to the same thing it already is)
+              // triggers a full re-render, if the span is one in the results
+              // collection.  A full re-render slows us down and disrupts events
+              // like double-clicking.
+              if (that.span === span) {
+                if (!span.get("selected")) {
+                  span.set("selected", true);
+                }
+              } else if (span.get("selected")) {
+                span.set("selected", false);
+              }
+            });
+        }
+        return true;
+      case "draw":
+        this.draw();
+        return true;
+      case "dblclick":
+        if (htrace.inBoundingBox(e.x, e.y,
+            this.x0, this.xF, this.y0, this.yF)) {
+          htrace.showSpanDetails(this.span);
+        }
+        return true;
+    }
+  };
+
+  for (var k in params) {
+    this[k]=params[k];
+  }
+  this.xSize = this.xF - this.x0;
+  this.ySize = this.yF - this.y0;
+  this.xDB = this.xD - this.xB;
+  this.manager.register("draw", this);
+
+  var widget = this;
+  if ((this.span.get("reifiedParents") == null) && (this.allowUpButton)) {
+    new htrace.TriangleButton({
+      ctx: this.ctx,
+      manager: this.manager,
+      direction: "up",
+      x0: this.xB + 2,
+      xF: this.xB + (this.xDB / 2) - 2,
+      y0: this.y0 + 2,
+      yF: this.yF - 2,
+      callback: function() {
+        $.when(widget.span.reifyParents()).done(function (result) {
+          console.log("reifyParents: result was '" + result + "'");
+          if (result != "") {
+            alert(result);
+          } else {
+            widget.manager.searchResultsView.render();
+          }
+        });
+      },
+    });
+  }
+  if ((this.span.get("reifiedChildren") == null) && (this.allowDownButton)) {
+    new htrace.TriangleButton({
+      ctx: this.ctx,
+      manager: this.manager,
+      direction: "down",
+      x0: this.xB + (this.xDB / 2) + 2,
+      xF: this.xD - 2,
+      y0: this.y0 + 2,
+      yF: this.yF - 2,
+      callback: function() {
+        $.when(widget.span.reifyChildren()).done(function (result) {
+          console.log("reifyChildren: result was '" + result + "'");
+          if (result != "") {
+            alert(result);
+          } else {
+            widget.manager.searchResultsView.render();
+          }
+        });
+      },
+    });
+  }
+  this.manager.register("mouseDown", this);
+  this.manager.register("dblclick", this);
+  return this;
+};

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/05ce37fb/htrace-webapp/src/main/webapp/app/string.js
----------------------------------------------------------------------
diff --git a/htrace-webapp/src/main/webapp/app/string.js b/htrace-webapp/src/main/webapp/app/string.js
new file mode 100644
index 0000000..c9c514b
--- /dev/null
+++ b/htrace-webapp/src/main/webapp/app/string.js
@@ -0,0 +1,62 @@
+/*
+ * 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 htrace = htrace || {};
+
+// Parse an ISO8601 date string into a moment.js object.
+htrace.parseDate = function(val) {
+  if (val.match(/^[0-9]([0-9]*)$/)) {
+    // Treat an all-numeric field as UTC milliseconds since the epoch.
+    return moment.utc(parseInt(val, 10));
+  }
+  // Look for approved date formats.
+  var toTry = [
+    "YYYY-MM-DDTHH:mm:ss,SSS",
+    "YYYY-MM-DDTHH:mm:ss",
+    "YYYY-MM-DDTHH:mm",
+    "YYYY-MM-DD"
+  ];
+  for (var i = 0; i < toTry.length; i++) {
+    var m = moment.utc(val, toTry[i], true);
+    if (m.isValid()) {
+      return m;
+    }
+  }
+  throw "Please enter the date either as YYYY-MM-DDTHH:mm:ss,SSS " +
+      "in UTC, or as the number of milliseconds since the epoch.";
+};
+
+// Convert a moment.js moment into an ISO8601-style date string.
+htrace.dateToString = function(val) {
+  return moment.utc(val).format("YYYY-MM-DDTHH:mm:ss,SSS");
+};
+
+// Normalize a span ID into the format the server expects to see--
+// i.e. something like 00000000000000000000000000000000.
+htrace.normalizeSpanId = function(str) {
+  if (str.length != 36) {
+    throw "The length of '" + str + "' was " + str.length +
+      ", but span IDs must be 36 characters long.";
+  }
+  if (str.search(/[^0-9a-fA-F]/) != -1) {
+    throw "Span IDs must contain only hexadecimal digits, but '" + str +
+      "' contained invalid characters.";
+  }
+  return str;
+};

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/05ce37fb/htrace-webapp/src/main/webapp/app/time_cursor.js
----------------------------------------------------------------------
diff --git a/htrace-webapp/src/main/webapp/app/time_cursor.js b/htrace-webapp/src/main/webapp/app/time_cursor.js
new file mode 100644
index 0000000..1caaa9a
--- /dev/null
+++ b/htrace-webapp/src/main/webapp/app/time_cursor.js
@@ -0,0 +1,81 @@
+/*
+ * 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 htrace = htrace || {};
+
+// Draws a vertical bar selecting a time.
+htrace.TimeCursor = function(params) {
+  this.positionToTime = function(x) {
+    if ((x < this.x0) || (x > this.xF)) {
+      return -1;
+    }
+    return this.begin +
+      (((x - this.x0) * (this.end - this.begin)) / (this.xF - this.x0));
+  };
+
+  this.timeToPosition = function(time) {
+    return this.x0 + (((time - this.begin) *
+        (this.xF - this.x0)) / (this.end - this.begin));
+  };
+
+  this.draw = function() {
+    if (this.selectedTime != -1) {
+      this.ctx.save();
+      this.ctx.beginPath();
+      this.ctx.rect(this.x0, this.y0,
+          this.xF - this.x0, this.yF - this.y0);
+      this.ctx.clip();
+      this.ctx.strokeStyle="#ff0000";
+      var x = this.timeToPosition(this.selectedTime);
+      this.ctx.beginPath();
+      this.ctx.moveTo(x, this.y0);
+      this.ctx.lineTo(x, this.yF);
+      this.ctx.stroke();
+      this.ctx.restore();
+    }
+  };
+
+  this.handle = function(e) {
+    switch (e.type) {
+      case "mouseMove":
+        if (htrace.inBoundingBox(e.x, e.y,
+              this.x0, this.xF, this.y0, this.yF)) {
+          this.selectedTime = this.positionToTime(e.x);
+          if (this.selectedTime < 0) {
+            $(this.el).val("");
+          } else {
+            $(this.el).val(htrace.dateToString(this.selectedTime));
+          }
+          return true;
+        }
+        return true;
+      case "draw":
+        this.draw();
+        return true;
+    }
+  };
+
+  this.selectedTime = -1;
+  for (var k in params) {
+    this[k]=params[k];
+  }
+  this.manager.register("mouseMove", this);
+  this.manager.register("draw", this);
+  return this;
+};

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/05ce37fb/htrace-webapp/src/main/webapp/app/tree.js
----------------------------------------------------------------------
diff --git a/htrace-webapp/src/main/webapp/app/tree.js b/htrace-webapp/src/main/webapp/app/tree.js
new file mode 100644
index 0000000..046085c
--- /dev/null
+++ b/htrace-webapp/src/main/webapp/app/tree.js
@@ -0,0 +1,74 @@
+/*
+ * 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 htrace = htrace || {};
+
+//
+// Get the height of a tree-- that is, the number of edges on the longest
+// downward path between the root and a leaf
+//
+htrace.treeHeight = function(node, getDescendants) {
+  var height = 0;
+  var descendants = getDescendants(node);
+  for (var i = 0; i < descendants.length; i++) {
+    height = Math.max(height,
+        1 + htrace.treeHeight(descendants[i], getDescendants));
+  }
+  return height;
+};
+
+//
+// Perform a depth-first, post-order traversal on the tree, invoking the
+// callback on every node with the node and depth as the arguments.
+//
+// Example:
+//     5
+//    / \
+//   3   4
+//  / \
+// 1   2
+//
+htrace.treeTraverseDepthFirstPost = function(node, getDescendants, depth, cb) {
+  var descendants = getDescendants(node);
+  for (var i = 0; i < descendants.length; i++) {
+    htrace.treeTraverseDepthFirstPost(descendants[i],
+        getDescendants, depth + 1, cb);
+  }
+  cb(node, depth);
+};
+
+//
+// Perform a depth-first, pre-order traversal on the tree, invoking the
+// callback on every node with the node and depth as the arguments.
+//
+// Example:
+//     1
+//    / \
+//   2   5
+//  / \
+// 3   4
+//
+htrace.treeTraverseDepthFirstPre = function(node, getDescendants, depth, cb) {
+  cb(node, depth);
+  var descendants = getDescendants(node);
+  for (var i = 0; i < descendants.length; i++) {
+    htrace.treeTraverseDepthFirstPre(descendants[i],
+        getDescendants, depth + 1, cb);
+  }
+};

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/05ce37fb/htrace-webapp/src/main/webapp/app/triangle_button.js
----------------------------------------------------------------------
diff --git a/htrace-webapp/src/main/webapp/app/triangle_button.js b/htrace-webapp/src/main/webapp/app/triangle_button.js
new file mode 100644
index 0000000..f252476
--- /dev/null
+++ b/htrace-webapp/src/main/webapp/app/triangle_button.js
@@ -0,0 +1,108 @@
+/*
+ * 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 htrace = htrace || {};
+
+// Triangle button widget.
+htrace.TriangleButton = function(params) {
+  this.fgColor = "#6600ff";
+  this.bgColor = "#ffffff";
+  this.selected = false;
+  this.direction = "down";
+
+  this.draw = function() {
+    this.ctx.save();
+    var fg = this.selected ? this.bgColor : this.fgColor;
+    var bg = this.selected ? this.fgColor : this.bgColor;
+    this.ctx.beginPath();
+    this.ctx.rect(this.x0, this.y0,
+        this.xF - this.x0, this.yF - this.y0);
+    this.ctx.clip();
+    this.ctx.fillStyle = bg;
+    this.ctx.strokeStyle = fg;
+    this.ctx.fillRect(this.x0, this.y0,
+        this.xF - this.x0, this.yF - this.y0);
+    this.ctx.lineWidth = 3;
+    this.ctx.strokeRect(this.x0, this.y0,
+        this.xF - this.x0, this.yF - this.y0);
+    var xPad = (this.xF - this.x0) / 5;
+    var yPad = (this.yF - this.y0) / 5;
+    this.ctx.fillStyle = fg;
+    this.ctx.strokeStyle = fg;
+    this.ctx.beginPath();
+    this.ctx.strokeStyle = fg;
+    if (this.direction === "up") {
+      this.ctx.moveTo(Math.floor(this.x0 + ((this.xF - this.x0) / 2)),
+          this.y0 + yPad);
+      this.ctx.lineTo(this.xF - xPad, this.yF - yPad);
+      this.ctx.lineTo(this.x0 + xPad, this.yF - yPad);
+    } else if (this.direction === "down") {
+      this.ctx.moveTo(this.x0 + xPad, this.y0 + yPad);
+      this.ctx.lineTo(this.xF - xPad, this.y0 + yPad);
+      this.ctx.lineTo(Math.floor(this.x0 + ((this.xF - this.x0) / 2)),
+          this.yF - yPad);
+    } else {
+      console.log("TriangleButton: unknown direction " + this.direction);
+    }
+    this.ctx.closePath();
+    this.ctx.fill();
+    this.ctx.restore();
+  };
+
+  this.handle = function(e) {
+    switch (e.type) {
+      case "mouseDown":
+        if (!htrace.inBoundingBox(e.x, e.y,
+              this.x0, this.xF, this.y0, this.yF)) {
+          return true;
+        }
+        this.manager.register("mouseUp", this);
+        this.manager.register("mouseMove", this);
+        this.manager.register("mouseOut", this);
+        this.selected = true;
+        return false;
+      case "mouseUp":
+        if (this.selected) {
+          this.callback();
+          this.selected = false;
+        }
+        this.manager.unregister("mouseUp", this);
+        this.manager.unregister("mouseMove", this);
+        this.manager.unregister("mouseOut", this);
+        return true;
+      case "mouseMove":
+        this.selected = htrace.inBoundingBox(e.x, e.y,
+                this.x0, this.xF, this.y0, this.yF);
+        return true;
+      case "mouseOut":
+        this.selected = false;
+        return true;
+      case "draw":
+        this.draw();
+        return true;
+    }
+  };
+
+  for (var k in params) {
+    this[k]=params[k];
+  }
+  this.manager.register("mouseDown", this);
+  this.manager.register("draw", this);
+  return this;
+};

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/05ce37fb/htrace-webapp/src/main/webapp/app/widget_manager.js
----------------------------------------------------------------------
diff --git a/htrace-webapp/src/main/webapp/app/widget_manager.js b/htrace-webapp/src/main/webapp/app/widget_manager.js
new file mode 100644
index 0000000..e519485
--- /dev/null
+++ b/htrace-webapp/src/main/webapp/app/widget_manager.js
@@ -0,0 +1,67 @@
+/*
+ * 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 htrace = htrace || {};
+
+// Check if a point is inside a bounding box.
+htrace.inBoundingBox = function(x, y, x0, xF, y0, yF) {
+    return ((x >= x0) && (x <= xF) && (y >= y0) && (y <= yF));
+  }
+
+// Manages a set of widgets on the canvas.
+// Buttons and sliders are both widgets.
+htrace.WidgetManager = function(params) {
+  this.listeners = {
+    "mouseDown": [],
+    "mouseUp": [],
+    "mouseMove": [],
+    "mouseOut": [],
+    "dblclick": [],
+    "draw": [],
+  };
+
+  this.register = function(type, widget) {
+    this.listeners[type].push(widget);
+  }
+
+  this.registerHighPriority = function(type, widget) {
+    this.listeners[type].unshift(widget);
+  }
+
+  this.unregister = function(type, widget) {
+    this.listeners[type] = _.without(this.listeners[type], widget);
+  }
+
+  this.handle = function(e) {
+    // Make a copy of the listeners, in case the handling functions change the
+    // array.
+    var listeners = this.listeners[e.type].slice();
+    var len = listeners.length;
+    for (var i = 0; i < len; i++) {
+      if (!listeners[i].handle(e)) {
+        break;
+      }
+    }
+  };
+
+  for (var k in params) {
+    this[k]=params[k];
+  }
+  return this;
+};

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/05ce37fb/htrace-webapp/src/main/webapp/custom.css
----------------------------------------------------------------------
diff --git a/htrace-webapp/src/main/webapp/custom.css b/htrace-webapp/src/main/webapp/custom.css
new file mode 100644
index 0000000..17945cb
--- /dev/null
+++ b/htrace-webapp/src/main/webapp/custom.css
@@ -0,0 +1,101 @@
+/*!
+ * 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.
+ */
+
+.navbar-default {
+  background-color: #001da9;
+  border-color: #b2b5db;
+}
+.navbar-default .navbar-brand {
+  color: #ecf0f1;
+}
+.navbar-default .navbar-brand:hover, .navbar-default .navbar-brand:focus {
+  color: #ffffff;
+}
+.navbar-default .navbar-text {
+  color: #ecf0f1;
+}
+.navbar-default .navbar-nav > li > a {
+  color: #ecf0f1;
+}
+.navbar-default .navbar-nav > li > a:hover, .navbar-default .navbar-nav > li > a:focus {
+  color: #ffffff;
+}
+.navbar-default .navbar-nav > li > .dropdown-menu {
+  background-color: #001da9;
+}
+.navbar-default .navbar-nav > li > .dropdown-menu > li > a {
+  color: #ecf0f1;
+}
+.navbar-default .navbar-nav > li > .dropdown-menu > li > a:hover,
+.navbar-default .navbar-nav > li > .dropdown-menu > li > a:focus {
+  color: #ffffff;
+  background-color: #b2b5db;
+}
+.navbar-default .navbar-nav > li > .dropdown-menu > li > .divider {
+  background-color: #001da9;
+}
+.navbar-default .navbar-nav > .active > a, .navbar-default .navbar-nav > .active > a:hover, .navbar-default .navbar-nav > .active > a:focus {
+  color: #ffffff;
+  background-color: #b2b5db;
+}
+.navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .open > a:hover, .navbar-default .navbar-nav > .open > a:focus {
+  color: #ffffff;
+  background-color: #b2b5db;
+}
+.navbar-default .navbar-toggle {
+  border-color: #b2b5db;
+}
+.navbar-default .navbar-toggle:hover, .navbar-default .navbar-toggle:focus {
+  background-color: #b2b5db;
+}
+.navbar-default .navbar-toggle .icon-bar {
+  background-color: #ecf0f1;
+}
+.navbar-default .navbar-collapse,
+.navbar-default .navbar-form {
+  border-color: #ecf0f1;
+}
+.navbar-default .navbar-link {
+  color: #ecf0f1;
+}
+.navbar-default .navbar-link:hover {
+  color: #ffffff;
+}
+.htrace-canvas-container {
+  overflow: hidden;
+  position: relative;
+}
+.htrace-canvas {
+  position: absolute;
+  top: 0px;
+  left: 0px;
+}
+
+@media (max-width: 767px) {
+  .navbar-default .navbar-nav .open .dropdown-menu > li > a {
+    color: #ecf0f1;
+  }
+  .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {
+    color: #ffffff;
+  }
+  .navbar-default .navbar-nav .open .dropdown-menu > .active > a, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {
+    color: #ffffff;
+    background-color: #b2b5db;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/05ce37fb/htrace-webapp/src/main/webapp/image/owl.png
----------------------------------------------------------------------
diff --git a/htrace-webapp/src/main/webapp/image/owl.png b/htrace-webapp/src/main/webapp/image/owl.png
new file mode 100644
index 0000000..be6fabd
Binary files /dev/null and b/htrace-webapp/src/main/webapp/image/owl.png differ

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/05ce37fb/htrace-webapp/src/main/webapp/index.html
----------------------------------------------------------------------
diff --git a/htrace-webapp/src/main/webapp/index.html b/htrace-webapp/src/main/webapp/index.html
new file mode 100644
index 0000000..ec28fe6
--- /dev/null
+++ b/htrace-webapp/src/main/webapp/index.html
@@ -0,0 +1,246 @@
+<!doctype html>
+<!--
+   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.
+-->
+<html lang="en-US">
+  <head>
+    <title>HTrace</title>
+    <meta charset="utf-8" name="viewport"
+      content="width=device-width, initial-scale=1.0">
+    <link href="lib/bootstrap-3.3.1/css/bootstrap.css" rel="stylesheet" type="text/css">
+    <link href="custom.css" rel="stylesheet" type="text/css">
+  </head>
+  <body>
+    <header id="header" role="banner">
+      <nav class="navbar navbar-default navbar-static-top" role="navigation">
+        <div class="collapse navbar-collapse">
+          <a class="navbar-brand" href="#">HTrace</a>
+          <ul class="nav navbar-nav">
+            <li id="about"><a href="#about">About</a></li>
+            <li id="search"><a href="#search">Search</a></li>
+          </ul>
+        </div>
+      </nav>
+    </header>
+    <div id="app" class="container-fluid" role="application"></div>
+    <div id="modal" class="modal fade"></div>
+    <footer></footer>
+
+    <script id="about-view-template" type="text/template">
+      <div class="row">
+        <div class="col-md-1">
+        </div>
+        <div class="col-md-10">
+          <h1>Welcome to HTrace</h1>
+          <img src="image/owl.png" width="15%">
+          <h2>Server Version</h2>
+          <%= model.get("ReleaseVersion") %>
+          <h2>Server Git Hash</h2>
+          <%= model.get("GitVersion") %>
+        </div>
+        <div class="col-md-1">
+        </div>
+      </div>
+    </script>
+
+    <script id="search-view-template" type="text/template">
+      <div class="row" id="searchView">
+        <div class="col-md-3" role="form">
+          <div class="panel panel-default">
+            <div class="panel-heading">
+              <h1 class="panel-title">Timeline</h1>
+            </div style="border: 1px solid #000000;">
+            <div class="panel-body">
+              <div class="form-horizontal">
+                <div class="form-group">
+                  <label class="col-sm-2 control-label">Begin</label>
+                  <div class="col-sm-10">
+                    <input type="text" class="form-control" id="begin" value="1970-01-01T00:00:00,000"/>
+                  </div>
+                </div>
+                <div class="form-group">
+                  <label class="col-sm-2 control-label">End</label>
+                  <div class="col-sm-10">
+                    <input type="text" class="form-control" id="end" value="1970-01-01T00:00:00,100"/>
+                  </div>
+                </div>
+                <div class="form-group">
+                  <label class="col-sm-2 control-label">Cur</label>
+                  <div class="col-sm-10">
+                    <input type="text" class="form-control" id="selectedTime" value=""/>
+                  </div>
+                </div>
+                <div class="form-horizontal">
+                  <button type="button" class="btn btn btn-warning"
+                      id="zoomButton">Zoom</button>
+                </div>
+              </div>
+            </div>
+          </div>
+          <div class="panel panel-default">
+            <div class="panel-heading">
+              <h1 class="panel-title">Search</h1>
+            </div style="border: 1px solid #000000;">
+            <div class="panel-body">
+              <form>
+                <div id="predicates">
+                </div>
+                <div class="form-group">
+                  <div class="btn-group">
+                    <button type="button" data-toggle="dropdown"
+                          aria-expanded="false"
+                          class="btn btn-default dropdown-toggle">
+                      Add Predicate<span class="caret"></span>
+                    </button>
+                    <ul class="dropdown-menu" role="menu">
+                      <li><a href="javascript:void(0)" 
+                        class="add-field">Began after</a></li>
+                      <li><a href="javascript:void(0)" 
+                        class="add-field">Began at or before</a></li>
+                      <li><a href="javascript:void(0)" 
+                        class="add-field">Ended after</a></li>
+                      <li><a href="javascript:void(0)" 
+                        class="add-field">Ended at or before</a></li>
+                      <li><a href="javascript:void(0)" 
+                        class="add-field">Description contains</a></li>
+                      <li><a href="javascript:void(0)" 
+                        class="add-field">Description is exactly</a></li>
+                      <li><a href="javascript:void(0)" 
+                        class="add-field">Duration is longer than</a></li>
+                      <li><a href="javascript:void(0)" 
+                        class="add-field">Duration is at most</a></li>
+                      <li><a href="javascript:void(0)" 
+                        class="add-field">Span ID is</a></li>
+                      <li><a href="javascript:void(0)" 
+                        class="add-field">TracerId contains</a></li>
+                      <li><a href="javascript:void(0)" 
+                        class="add-field">TracerId is exactly</a></li>
+                    </ul>
+                  </div>
+                  <button type="submit" class="btn btn-primary" id="searchButton">
+                    Search</button>
+                </div>
+                <div class="form-group">
+                  <button type="button" class="btn btn btn-danger" id="clearButton">
+                    Clear</button>
+                </div>
+              </form>
+            </div>
+          </div>
+        </div>
+        <div class="col-md-9" id="resultsView">
+        </div>
+      </div>
+    </script>
+
+    <script id="predicate-template" type="text/template">
+      <form class="form-horizontal">
+        <div class="form-group"> 
+          <%= desc %>
+          <button type="button" class="btn pull-right btn-link btn-sm closeButton"
+              >X</button><br/>
+          <input type="text" class="form-control"/>
+        </div>
+      </form>
+    </script>
+
+    <script id="search-results-view-template" type="text/template">
+      <!-- tabindex=1 is needed or else the canvas can never gain mouse focus on Chrome. -->
+      <canvas id="resultsCanvas" class="htrace-canvas" tabindex="1">
+        <h2>Sorry, your browser does not support the HTML5 canvas element.  Please
+        upgrade to a newer browser.</h2>
+      </canvas>
+    </script>
+
+    <script id="modal-warning-template" type="text/template">
+      <div class="modal-dialog">
+        <div class="modal-content">
+          <div class="modal-header">
+            <button type="button" class="close" data-dismiss="modal"
+                  aria-label="Close">
+              <span aria-hidden="true">&times;</span>
+            </button>
+            <h4 class="modal-title"><%= title %></h4>
+          </div>
+          <div class="modal-body">
+            <%= body %><p/>
+          </div>
+          <div class="modal-footer">
+            <button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
+          </div>
+        </div>
+      </div>
+    </script>
+
+    <script id="modal-table-template" type="text/template">
+      <div class="modal-dialog modal-lg">
+        <div class="modal-content">
+          <div class="modal-header">
+            <button type="button" class="close" data-dismiss="modal"
+                  aria-label="Close">
+              <span aria-hidden="true">&times;</span>
+            </button>
+            <h4 class="modal-title"><%= title %></h4>
+          </div>
+          <div class="modal-body">
+            <%= body %><p/>
+          </div>
+          <div class="modal-footer">
+            <button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
+          </div>
+        </div>
+      </div>
+    </script>
+
+    <script id="table-row-template" type="text/template">
+      <tr bgcolor="<%= bgcolor %>">
+        <td style="width:30%;word-wrap:break-word"><%- key %></td>
+        <td style="width:70%;word-wrap:break-word"><%- val %></td>
+      </tr>
+    </script>
+
+    <script src="lib/jquery-2.1.4.js" type="text/javascript"></script>
+    <script src="lib/bootstrap-3.3.1/js/bootstrap.min.js" type="text/javascript"></script>
+    <script src="lib/underscore-1.7.0.js" type="text/javascript"></script>
+    <script src="lib/backbone-1.1.2.js" type="text/javascript"></script>
+    <script src="lib/moment-2.10.3.js" type="text/javascript"></script>
+
+    <script src="app/string.js" type="text/javascript"></script>
+    <script src="app/tree.js" type="text/javascript"></script>
+    <script src="app/time_cursor.js" type="text/javascript"></script>
+
+    <script src="app/widget_manager.js" type="text/javascript"></script>
+    <script src="app/triangle_button.js" type="text/javascript"></script>
+    <script src="app/partition_widget.js" type="text/javascript"></script>
+
+    <script src="app/span.js" type="text/javascript"></script>
+
+    <script src="app/span_group_widget.js" type="text/javascript"></script>
+    <script src="app/span_widget.js" type="text/javascript"></script>
+    <script src="app/search_results.js" type="text/javascript"></script>
+    <script src="app/about_view.js" type="text/javascript"></script>
+    <script src="app/modal.js" type="text/javascript"></script>
+    <script src="app/predicate.js" type="text/javascript"></script>
+    <script src="app/predicate_view.js" type="text/javascript"></script>
+    <script src="app/query_results.js" type="text/javascript"></script>
+    <script src="app/search_results_view.js" type="text/javascript"></script>
+    <script src="app/search_view.js" type="text/javascript"></script>
+    <script src="app/server_info.js" type="text/javascript"></script>
+
+    <script src="app/router.js" type="text/javascript"></script>
+  </body>
+</html>
+