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

[18/18] incubator-htrace git commit: HTRACE-174. Refactor GUI (cmccabe)

HTRACE-174. Refactor GUI (cmccabe)


Project: http://git-wip-us.apache.org/repos/asf/incubator-htrace/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-htrace/commit/baaa3a52
Tree: http://git-wip-us.apache.org/repos/asf/incubator-htrace/tree/baaa3a52
Diff: http://git-wip-us.apache.org/repos/asf/incubator-htrace/diff/baaa3a52

Branch: refs/heads/master
Commit: baaa3a5287ac7f096fbbdfc5cbb135050f22aca4
Parents: 4051054
Author: Colin P. Mccabe <cm...@apache.org>
Authored: Tue Jun 9 14:57:12 2015 -0700
Committer: Colin P. Mccabe <cm...@apache.org>
Committed: Tue Jun 9 14:57:12 2015 -0700

----------------------------------------------------------------------
 LICENSE.txt                                     |   30 +-
 README.md                                       |    2 +-
 htrace-htraced/BUILDING.txt                     |    2 +-
 .../src/go/src/org/apache/htrace/common/rest.go |    2 +-
 htrace-htraced/src/web/app/about_view.js        |   33 +
 htrace-htraced/src/web/app/app.js               |   20 -
 htrace-htraced/src/web/app/modal.js             |   34 +
 htrace-htraced/src/web/app/models/span.js       |  144 -
 htrace-htraced/src/web/app/predicate.js         |  117 +
 htrace-htraced/src/web/app/predicate_view.js    |   68 +
 htrace-htraced/src/web/app/query_results.js     |   45 +
 htrace-htraced/src/web/app/router.js            |   74 +
 htrace-htraced/src/web/app/search_results.js    |   25 +
 .../src/web/app/search_results_view.js          |  365 +
 htrace-htraced/src/web/app/search_view.js       |  197 +
 htrace-htraced/src/web/app/server_info.js       |   31 +
 htrace-htraced/src/web/app/setup.js             |  192 -
 htrace-htraced/src/web/app/span.js              |   69 +
 htrace-htraced/src/web/app/span_details_view.js |   39 +
 htrace-htraced/src/web/app/span_widget.js       |  229 +
 htrace-htraced/src/web/app/string.js            |   66 +
 htrace-htraced/src/web/app/time_cursor.js       |   74 +
 htrace-htraced/src/web/app/triangle_button.js   |  103 +
 .../src/web/app/views/details/details.js        |   47 -
 htrace-htraced/src/web/app/views/graph/graph.js |  262 -
 .../src/web/app/views/search/field.js           |  124 -
 .../src/web/app/views/search/search.js          |  105 -
 .../src/web/app/views/swimlane/swimlane.js      |  178 -
 htrace-htraced/src/web/app/widget_manager.js    |   66 +
 htrace-htraced/src/web/custom.css               |  101 +
 htrace-htraced/src/web/index.html               |  306 +-
 htrace-htraced/src/web/lib/backbone-1.1.2.js    | 1608 +++
 .../src/web/lib/css/backgrid-0.3.5.min.css      |    1 -
 .../lib/css/backgrid-paginator-0.3.5.min.css    |    1 -
 htrace-htraced/src/web/lib/css/main.css         |   45 -
 htrace-htraced/src/web/lib/jquery-2.1.4.js      | 9210 +++++++++++++++++
 htrace-htraced/src/web/lib/js/backbone-1.1.2.js | 1608 ---
 .../web/lib/js/backbone.marionette-2.4.1.min.js |   23 -
 .../src/web/lib/js/backbone.paginator-2.0.2.js  | 1325 ---
 htrace-htraced/src/web/lib/js/backgrid-0.3.5.js | 2883 ------
 .../src/web/lib/js/backgrid-paginator-0.3.5.js  |  433 -
 htrace-htraced/src/web/lib/js/d3-3.5.5.js       | 9504 ------------------
 .../src/web/lib/js/jquery-2.1.3.min.js          |    4 -
 .../src/web/lib/js/moment-2.9.0.min.js          |    7 -
 .../src/web/lib/js/underscore-1.7.0.js          | 1416 ---
 htrace-htraced/src/web/lib/moment-2.10.3.js     | 3111 ++++++
 htrace-htraced/src/web/lib/rome-2.1.0/rome.css  |   94 -
 htrace-htraced/src/web/lib/rome-2.1.0/rome.js   | 4796 ---------
 .../src/web/lib/rome-2.1.0/rome.min.css         |    2 -
 .../src/web/lib/rome-2.1.0/rome.min.js          |    3 -
 .../src/web/lib/rome-2.1.0/rome.standalone.js   | 1860 ----
 .../web/lib/rome-2.1.0/rome.standalone.min.js   |    2 -
 htrace-htraced/src/web/lib/underscore-1.7.0.js  | 1416 +++
 pom.xml                                         |    9 +-
 54 files changed, 17253 insertions(+), 25258 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/baaa3a52/LICENSE.txt
----------------------------------------------------------------------
diff --git a/LICENSE.txt b/LICENSE.txt
index 19dd8f7..3404aff 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -244,43 +244,15 @@ backbone, is a javascript library, that is Copyright (c) 2010-2014
 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:
 https://github.com/moment/moment/blob/develop/LICENSE
 
-rome.js is a customizable date (and time) picker.
-It is Copyright © 2014 Nicolas Bevacqua
-https://github.com/bevacqua/rome
-
-Backbone.Wreqr is a message passing system for Backbone.js.
-It is (c) 2012 Derick Bailey, Muted Solutions, LLC and MIT licensed:
-https://github.com/marionettejs/backbone.wreqr/blob/v1.3.1/LICENSE.md
-
-Backbone.Babysitter manages child views for Backbone.js.
-It is (c) 2013 Derick Bailey, Muted Solutions, LLC and MIT licensed:
-https://github.com/marionettejs/backbone.babysitter/blob/v0.1.6/LICENSE.md
-
-Backbone.Marionette is a composite application library for Backbone.js.
-It is MIT licensed:
-https://github.com/marionettejs/backbone.marionette/blob/v2.4.1/license.txt
-
 CMP is an implementation of the MessagePack serialization format in
 C.  It is licensed under the MIT license:
 https://github.com/camgunz/cmp/blob/master/LICENSE
 
 go-codec is an implementation of several serialization and deserialization
 codecs in Go.  It is licensed under the MIT license:
-https://github.com/ugorji/go/blob/master/LICENSE
\ No newline at end of file
+https://github.com/ugorji/go/blob/master/LICENSE

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/baaa3a52/README.md
----------------------------------------------------------------------
diff --git a/README.md b/README.md
index b3b486e..d469e22 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,6 @@
 
 HTrace
 ======
-HTrace is a tracing framework for use with distributed systems written in java.
+HTrace is a tracing framework for use with distributed systems.
 
 See documentation at src/main/site/markdown/index.md or at http://htrace.incubator.apache.org.

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/baaa3a52/htrace-htraced/BUILDING.txt
----------------------------------------------------------------------
diff --git a/htrace-htraced/BUILDING.txt b/htrace-htraced/BUILDING.txt
index d54d410..abc0113 100644
--- a/htrace-htraced/BUILDING.txt
+++ b/htrace-htraced/BUILDING.txt
@@ -7,7 +7,7 @@ The htrace go code consists of 4 main parts:
 * The "htrace" command-line program which can query the server
     This is a simple command-line program which can query the htrace server.
 
-* The htraced Javascript Web UI (not yet implemented)
+* The htraced Javascript Web UI
 
 * The htrace go client library (not yet implemented)
     This is the equivalent of the Java HTrace client library, but written in Go.

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/baaa3a52/htrace-htraced/src/go/src/org/apache/htrace/common/rest.go
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/go/src/org/apache/htrace/common/rest.go b/htrace-htraced/src/go/src/org/apache/htrace/common/rest.go
index eeb9568..b898ca4 100644
--- a/htrace-htraced/src/go/src/org/apache/htrace/common/rest.go
+++ b/htrace-htraced/src/go/src/org/apache/htrace/common/rest.go
@@ -19,7 +19,7 @@
 
 package common
 
-// Info returned by /serverInfo
+// Info returned by /server/info
 type ServerInfo struct {
 	// The server release version.
 	ReleaseVersion string

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/baaa3a52/htrace-htraced/src/web/app/about_view.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/about_view.js b/htrace-htraced/src/web/app/about_view.js
new file mode 100644
index 0000000..7dfe868
--- /dev/null
+++ b/htrace-htraced/src/web/app/about_view.js
@@ -0,0 +1,33 @@
+/*
+ * 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.AboutView = Backbone.View.extend({
+  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/baaa3a52/htrace-htraced/src/web/app/app.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/app.js b/htrace-htraced/src/web/app/app.js
deleted file mode 100644
index 0bc7100..0000000
--- a/htrace-htraced/src/web/app/app.js
+++ /dev/null
@@ -1,20 +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.
- */
-
-window.app = new Marionette.Application();

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/baaa3a52/htrace-htraced/src/web/app/modal.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/modal.js b/htrace-htraced/src/web/app/modal.js
new file mode 100644
index 0000000..91d55fe
--- /dev/null
+++ b/htrace-htraced/src/web/app/modal.js
@@ -0,0 +1,34 @@
+/*
+ * 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 || {};
+
+// Show a modal dialog box with a warning message.
+htrace.showModalWarning = function(title, body) {
+  var html = _.template($("#modal-warning-template").html())
+      ({ title: title, body: body });
+  htrace.showModal(html);
+}
+
+// Show a modal dialog box.
+htrace.showModal = function(html) {
+  var el = $("#modal");
+  el.html(html);
+  el.modal();
+}

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/baaa3a52/htrace-htraced/src/web/app/models/span.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/models/span.js b/htrace-htraced/src/web/app/models/span.js
deleted file mode 100644
index b8dc114..0000000
--- a/htrace-htraced/src/web/app/models/span.js
+++ /dev/null
@@ -1,144 +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.
- */
-
-// Span model
-app.Span = Backbone.Model.extend({
-  "defaults": {
-    "spanId": null,
-    "traceId": null,
-    "processId": null,
-    "parents": null,
-    "description": null,
-    "beginTime": 0,
-    "stopTime": 0
-  },
-
-  shorthand: {
-    "s": "spanId",
-    "b": "beginTime",
-    "e": "stopTime",
-    "d": "description",
-    "r": "processId",
-    "p": "parents",
-    "i": "traceId"
-  },
-
-  parse: function(response, options) {
-    var attrs = {};
-    var $this = this;
-    $.each(response, function(key, value) {
-      attrs[(key in $this.shorthand) ? $this.shorthand[key] : key] = value;
-    });
-    return attrs;
-  },
-
-  duration: function() {
-    return this.get('stopTime') - this.get('beginTime');
-  }
-});
-
-app.Spans = Backbone.PageableCollection.extend({
-  model: app.Span,
-  mode: "infinite",
-  url: "/query",
-  state: {
-    pageSize: 10,
-    lastSpanId: null,
-    finished: false,
-    predicates: []
-  },
-  queryParams: {
-    totalPages: null,
-    totalRecords: null,
-    firstPage: null,
-    lastPage: null,
-    currentPage: null,
-    pageSize: null,
-    sortKey: null,
-    order: null,
-    directions: null,
-
-    /**
-     * Query parameter for htraced.
-     */
-    query: function() {
-      var predicates = this.state.predicates.slice(0);
-      var lastSpanId = this.state.lastSpanId;
-
-      /**
-       * Use last pulled span ID to paginate.
-       * The htraced API works such that order is defined by the first predicate.
-       * Adding a predicate to the end of the predicates list won't change the order.
-       * Providing the predicate on spanid will filter all previous spanids.
-       */
-      if (lastSpanId) {
-        predicates.push({
-          "op": "gt",
-          "field": "spanid",
-          "val": lastSpanId
-        });
-      }
-
-      return JSON.stringify({
-        lim: this.state.pageSize + 1,
-        pred: predicates
-      });
-    }
-  },
-
-  initialize: function() {
-    this.on("reset", function(collection, response, options) {
-      if (response.length == 0) {
-        delete this.links[this.state.currentPage];
-        this.getPreviousPage();
-      }
-    }, this);
-  },
-
-  parseLinks: function(resp, xhr) {
-    this.state.finished = resp.length <= this.state.pageSize;
-
-    if (this.state.finished) {
-      this.state.lastSpanId = null;
-    } else {
-      this.state.lastSpanId = resp[this.state.pageSize - 1].s;
-    }
-
-    if (this.state.finished) {
-      return {};
-    }
-
-    return {
-      "next": "/query?query=" + this.queryParams.query.call(this)
-    };
-  },
-
-  parseRecords: function(resp) {
-    return resp.slice(0, 10);
-  },
-
-  setPredicates: function(predicates) {
-    if (!$.isArray(predicates)) {
-      console.error("predicates should be an array");
-      return;
-    }
-
-    this.state.predicates = predicates;
-  }
-});

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/baaa3a52/htrace-htraced/src/web/app/predicate.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/predicate.js b/htrace-htraced/src/web/app/predicate.js
new file mode 100644
index 0000000..87a5602
--- /dev/null
+++ b/htrace-htraced/src/web/app/predicate.js
@@ -0,0 +1,117 @@
+/*
+ * 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.checkStringIsPositiveWholeNumber = function(val) {
+  if (!val.match(/^[0-9]([0-9]*)$/)) {
+    if (!val.match(/[^\s]/)) {
+      throw "You entered an empty string into a numeric field.";
+    }
+    throw "Non-numeric characters found.";
+  }
+};
+
+htrace.checkStringIsNotEmpty = function(val) {
+  if (!val.match(/[^\s]/)) {
+    throw "You entered an empty string into a text field.";
+  }
+};
+
+// Predicate type
+htrace.PType = Backbone.Model.extend({
+  initialize: function(options) {
+    this.name = options.name;
+    this.field = options.field;
+    this.op = options.op;
+  },
+
+  // Try to normalize a value of this type into something htraced can accept.
+  // Returns a string containing the normalized value on success.  Throws a
+  // string explaining the parse error otherwise.
+  // Dates are represented by milliseconds since the epoch; span ids don't start
+  // with 0x.
+  normalize: function(val) {
+    switch (this.field) {
+    case "begin":
+      return htrace.parseDate(val).valueOf().toString();
+    case "end":
+      return htrace.parseDate(val).valueOf().toString();
+    case "description":
+      htrace.checkStringIsNotEmpty(val);
+      return val;
+    case "duration":
+      htrace.checkStringIsPositiveWholeNumber(val);
+      return val;
+    case "spanid":
+      return htrace.normalizeSpanId(val);
+    default:
+      return "Normalization not implemented for field '" + this.field + "'";
+    }
+  },
+
+  getDefaultValue: function() {
+    switch (this.field) {
+    case "begin":
+      return htrace.dateToString(moment());
+    case "end":
+      return htrace.dateToString(moment());
+    case "description":
+      return "";
+    case "duration":
+      return "0";
+    case "spanid":
+      return "";
+    default:
+      return "(unknown)";
+    }
+  }
+});
+
+htrace.parsePType = function(name) {
+  switch (name) {
+    case "Began after":
+      return new htrace.PType({name: name, field:"begin", op:"gt"});
+    case "Began at or before":
+      return new htrace.PType({name: name, field:"begin", op:"le"});
+    case "Ended after":
+      return new htrace.PType({name: name, field:"end", op:"gt"});
+    case "Ended at or before":
+      return new htrace.PType({name: name, field:"end", op:"le"});
+    case "Description contains":
+      return new htrace.PType({name: name, field:"description", op:"cn"});
+    case "Description is exactly":
+      return new htrace.PType({name: name, field:"description", op:"eq"});
+    case "Duration is longer than":
+      return new htrace.PType({name: name, field:"duration", op:"gt"});
+    case "Duration is at most":
+      return new htrace.PType({name: name, field:"duration", op:"le"});
+    case "Span ID is":
+      return new htrace.PType({name: name, field:"spanid", op:"eq"});
+    default:
+      return null
+  }
+};
+
+htrace.Predicate = function(options) {
+  this.op = options.ptype.op;
+  this.field = options.ptype.field;
+  this.val = options.val;
+  return this;
+};

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/baaa3a52/htrace-htraced/src/web/app/predicate_view.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/predicate_view.js b/htrace-htraced/src/web/app/predicate_view.js
new file mode 100644
index 0000000..aefe896
--- /dev/null
+++ b/htrace-htraced/src/web/app/predicate_view.js
@@ -0,0 +1,68 @@
+/*
+ * 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.PredicateView = Backbone.View.extend({
+  initialize: function(options) {
+    this.el = options.el;
+    this.index = options.index;
+    this.ptype = options.ptype;
+    this.searchView = options.searchView;
+  },
+
+  events: {
+    "click .closeButton": "remove",
+  },
+
+  render: function() {
+    this.$el.html(_.template($("#predicate-template").html())
+        ({ desc: this.ptype.name, id: this.index }))
+    if (this.getText() === "") {
+      $(this.$el).find(".form-control").val(this.ptype.getDefaultValue());
+    }
+    console.log(this.toString() + "#render");
+    return this;
+  },
+
+  // Handle the user removing this predicate.
+  remove: function() {
+    this.searchView.removePredicateView(this);
+    Backbone.View.prototype.remove.apply(this, arguments);
+  },
+
+  // Get the text which the user has entered in.
+  getText: function() {
+    return $(this.$el).find(".form-control").val().trim();
+  },
+
+  // Get the predicate expressed by this view.
+  // Throw an exception if the predicate can't be parsed.
+  getPredicate: function() {
+    return new htrace.Predicate({
+        ptype: this.ptype,
+        val: this.ptype.normalize(this.getText())
+    });
+  },
+
+  toString: function() {
+    return "PredicateView(this.el=" + this.el + ", this.index=" +
+        this.index + ", this.ptype='" + this.ptype.name + "')";
+  }
+});

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/baaa3a52/htrace-htraced/src/web/app/query_results.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/query_results.js b/htrace-htraced/src/web/app/query_results.js
new file mode 100644
index 0000000..6fdde9f
--- /dev/null
+++ b/htrace-htraced/src/web/app/query_results.js
@@ -0,0 +1,45 @@
+/*
+ * 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.QueryResults = Backbone.Collection.extend({
+  // The query results are spans. 
+  model: htrace.Span,
+
+  initialize: function(options) {
+    this.queryJson = options.queryJson;
+  },
+
+  url: function() {
+    return "query?query=" + this.queryString();
+  },
+
+  parse: function(response, xhr) {
+    return response;
+  },
+
+  prettyQueryString: function() {
+    return JSON.stringify(this.queryJson, null, 2);
+  },
+
+  queryString: function() {
+    return JSON.stringify(this.queryJson);
+  }
+});

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/baaa3a52/htrace-htraced/src/web/app/router.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/router.js b/htrace-htraced/src/web/app/router.js
new file mode 100644
index 0000000..607da44
--- /dev/null
+++ b/htrace-htraced/src/web/app/router.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 || {};
+
+htrace.HTraceRouter = Backbone.Router.extend({
+  "routes": {
+    "": "empty",
+    "about": "about",
+    "search": "search",
+    "*unknown": "unknown"
+  },
+
+  empty: function() {
+    console.log("Redirecting to #about.");
+    Backbone.history.navigate("about", {"trigger": true, "replace": true});
+  },
+
+  about: function() {
+    console.log("Visiting #about.");
+    serverInfo = new htrace.ServerInfo();
+    var router = this;
+    serverInfo.fetch({
+        "success": function(model, response, options) {
+          router.switchView(new htrace.AboutView({model: serverInfo, el: "#app"}));
+          router.activateNavBarEntry("about")
+        },
+        "error": function(model, response, options) {
+          window.alert("Failed to fetch htraced server info via GET " +
+                       "/server/info: " + JSON.stringify(response));
+        }
+      });
+  },
+
+  search: function() {
+    console.log("Visiting #search.");
+    this.switchView(new htrace.SearchView({el : "#app"}));
+    htrace.router.activateNavBarEntry("search");
+  },
+
+  unknown: function() {
+    console.log("Unknown route " + Backbone.history.getFragment() + ".")
+  },
+
+  "switchView": function(view) {
+    this.view && this.view.close();
+    this.view = view;
+    this.view.render();
+  },
+
+  "activateNavBarEntry": function(id) {
+     $(".nav").find(".active").removeClass("active");
+     $(".nav").find("#" + id).addClass("active");
+  }
+});
+
+htrace.router = new htrace.HTraceRouter();
+Backbone.history.start();

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/baaa3a52/htrace-htraced/src/web/app/search_results.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/search_results.js b/htrace-htraced/src/web/app/search_results.js
new file mode 100644
index 0000000..d214918
--- /dev/null
+++ b/htrace-htraced/src/web/app/search_results.js
@@ -0,0 +1,25 @@
+/*
+ * 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.SearchResults = Backbone.Collection.extend({
+  // The search results are spans. 
+  model: htrace.Span
+});

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/baaa3a52/htrace-htraced/src/web/app/search_results_view.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/search_results_view.js b/htrace-htraced/src/web/app/search_results_view.js
new file mode 100644
index 0000000..b3473c4
--- /dev/null
+++ b/htrace-htraced/src/web/app/search_results_view.js
@@ -0,0 +1,365 @@
+/*
+ * 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.SearchResultsView = Backbone.View.extend({
+  // The minimum time span we will allow between begin and end.
+  MINIMUM_TIME_SPAN: 100,
+
+  begin: 0,
+
+  end: this.MINIMUM_TIME_SPAN,
+
+  focused: false,
+
+  initialize: function(options) {
+    this.model = options.searchResults;
+    this.el = options.el;
+    this.listenTo(this.model, 'add remove change reset', this.render);
+
+    // Re-render the canvas when the window size changes.
+    // Add a debouncer delay to avoid spamming render requests.
+    var view = this;
+    $(window).on("resize", _.debounce(function()  {
+      view.render();
+    }, 250));
+  },
+
+  // Get the canvas X coordinate of a mouse click from the absolute event
+  // coordinate.
+  getCanvasX: function(e) {
+    return e.pageX - $("#resultsCanvas").offset().left;
+  },
+
+  // Get the canvas Y coordinate of a mouse click from the absolute event
+  // coordinate.
+  getCanvasY: function(e) {
+    return e.pageY - $("#resultsCanvas").offset().top;
+  },
+
+  handleMouseDown: function(e) {
+    e.preventDefault();
+    var x = this.getCanvasX(e);
+    var y = this.getCanvasY(e);
+    var focused = this.widgetManager.handleMouseDown(x, y);
+    if (focused != this.focused) {
+      this.draw();
+      this.focused = focused;
+    }
+  },
+
+  handleMouseUp: function(e) {
+    e.preventDefault();
+    var x = this.getCanvasX(e);
+    var y = this.getCanvasY(e);
+    this.widgetManager.handleMouseUp(x, y);
+    this.focused = false;
+    this.draw();
+  },
+
+  // When the mouse leaves the canvas, treat it like a mouse up event at -1, -1
+  // if something is focused.
+  handleMouseOut: function(e) {
+    if (this.focused) {
+      this.widgetManager.handleMouseUp(-1, -1);
+      this.focused = false;
+      this.draw();
+    }
+  },
+
+  handleMouseMove: function(e) {
+    e.preventDefault();
+    var x = this.getCanvasX(e);
+    var y = this.getCanvasY(e);
+    if (this.focused) {
+      var mustDraw = false;
+      if (this.widgetManager.handleMouseMove(x, y)) {
+        mustDraw = true;
+      }
+    }
+    if (this.timeCursor.handleMouseMove(x, y)) {
+      mustDraw = true;
+    }
+    if (mustDraw) {
+      this.draw();
+    }
+  },
+
+  render: function() {
+    console.log("SearchResultsView#render.");
+    $(this.el).html(_.template($("#search-results-view-template").html()));
+    $('#selectedTime').attr('readonly', 'readonly');
+    this.canvas = $("#resultsCanvas");
+    this.ctx = this.canvas.get(0).getContext("2d");
+    this.scaleCanvas();
+    this.setupCoordinates();
+    this.setupTimeCursor();
+    this.setupWidgets();
+    this.draw();
+    this.attachEvents();
+    return this;
+  },
+
+  /*
+   * Compute the ratio to use between the size of the canvas (i.e.
+   * canvas.ctx.width, canvas.ctx.height) and the size in "HTML5 pixels." Note
+   * that 'HTML5 pixels" don't actually correspond to screen pixels.  A line 1
+   * "HTML5 pixel"  wide actually takes up multiple scren pixels, etc.
+   *
+   * TODO: fix this to be sharper
+   */
+  computeScaleFactor: function() {
+    var backingStoreRatio = this.ctx.backingStorePixelRatio ||
+          this.ctx.mozBackingStorePixelRatio ||
+          this.ctx.msBackingStorePixelRatio ||
+          this.ctx.webkitBackingStorePixelRatio ||
+          this.ctx.oBackingStorePixelRatio ||
+          this.ctx.backingStorePixelRatio || 1;
+    return (window.devicePixelRatio || 1) / backingStoreRatio;
+  },
+
+  // Sets up the canvas size and scaling.
+  scaleCanvas: function() {
+    var cssX = this.canvas.parent().innerWidth();
+    var cssY = $(window).innerHeight() - $("#header").innerHeight() - 50;
+    var ratio = this.computeScaleFactor();
+    console.log("scaleCanvas: cssX=" + cssX + ", cssY=" + cssY + ", ratio=" + ratio);
+    this.maxX = cssX;
+    this.maxY = cssY;
+    $('#searchView').css('height', cssY + "px");
+    $('#results').css('width', cssX + "px");
+    $('#results').css('height', cssY + "px");
+    $('#resultsView').css('width', cssX + "px");
+    $('#resultsView').css('height', cssY + "px");
+    $('#resultsDiv').css('width', cssX + "px");
+    $('#resultsDiv').css('height', cssY + "px");
+    $('#resultsCanvas').css('width', cssX + "px");
+    $('#resultsCanvas').css('height', cssY + "px");
+    this.ctx.canvas.width = cssX * ratio;
+    this.ctx.canvas.height = cssY * ratio;
+    this.ctx.scale(ratio, ratio);
+  },
+
+  //
+  // Set up the screen coordinates.
+  //
+  //  0              buttonX    descX                scrollX    maxX
+  //  +--------------+----------+--------------------+-----------+
+  //  |ProcessId     | Buttons  | Span Description   | Scrollbar |
+  //  +--------------+----------+--------------------+-----------+
+  //
+  setupCoordinates: function() {
+    this.buttonX = Math.min(300, Math.floor(this.maxX / 5));
+    this.descX = this.buttonX + Math.min(75, Math.floor(this.maxX / 20));
+    var scrollBarWidth = Math.min(50, Math.floor(this.maxX / 10));
+    this.scrollX = this.maxX - scrollBarWidth;
+  },
+
+  setupTimeCursor: function() {
+    var selectedTime;
+    if (this.timeCursor != null) {
+      selectedTime = this.timeCursor.selectedTime;
+      console.log("setupTimeCursor: selectedTime = (prev) " + selectedTime);
+    } else {
+      selectedTime = this.begin;
+      console.log("setupTimeCursor: selectedTime = (begin) " + selectedTime);
+    }
+    this.timeCursor = new htrace.TimeCursor({
+      ctx: this.ctx,
+      x0: this.descX,
+      xF: this.scrollX,
+      el: "#selectedTime",
+      y0: 0,
+      yF: this.maxY,
+      begin: this.begin,
+      end: this.end,
+      selectedTime: selectedTime
+    });
+  },
+
+  setupWidgets: function() {
+    var widgets = [];
+    var spanWidgetHeight = Math.min(25, Math.floor(this.maxY / 32));
+
+    // Create a SpanWidget for each span we know about
+    var numSpans = this.model.size();
+    for (var i = 0; i < numSpans; i++) {
+      var spanWidget = new htrace.SpanWidget({
+        ctx: this.ctx,
+        span: this.model.at(i),
+        x0: 0,
+        xB: this.buttonX,
+        xD: this.descX,
+        xF: this.scrollX,
+        y0: i * spanWidgetHeight,
+        yF: (i * spanWidgetHeight) + (spanWidgetHeight - 1),
+        begin: this.begin,
+        end: this.end
+      });
+      widgets.push(spanWidget);
+    }
+
+    // Create a new root-leve WidgetManager
+    this.widgetManager = new htrace.WidgetManager({
+      widgets: widgets
+    });
+  },
+
+  draw: function() {
+    if (this.checkCanvasTooSmall()) {
+      return;
+    }
+
+    // Set the background to white. 
+    this.ctx.save();
+    this.ctx.fillStyle="#ffffff";
+    this.ctx.strokeStyle="#000000";
+    this.ctx.fillRect(0, 0, this.maxX, this.maxY);
+    this.ctx.restore();
+
+    // Draw all the widgets.
+    this.widgetManager.draw();
+    this.timeCursor.draw();
+  },
+
+  checkCanvasTooSmall: function() {
+    if ((this.maxX < 200) || (this.maxY < 200)) {
+      this.ctx.fillStyle="#cccccc";
+      this.ctx.strokeStyle="#000000";
+      this.ctx.fillRect(0, 0, this.maxX, this.maxY);
+      this.ctx.font = "24px serif";
+      this.ctx.fillStyle="#000000";
+      this.ctx.fillText("Canvas too small!", 0, 24);
+      return true;
+    }
+    return false;
+  },
+
+  attachEvents: function() {
+    // Use jquery to capture mouse events on the canvas.
+    // For some reason using backbone doesn't work for getting these events.
+    var view = this;
+    $("#resultsCanvas").off("mousedown");
+    $("#resultsCanvas").on("mousedown", function(e) {
+      view.handleMouseDown(e);
+    });
+    $("#resultsCanvas").off("mouseup");
+    $("#resultsCanvas").on("mouseup", function(e) {
+      view.handleMouseUp(e);
+    });
+    $("#resultsCanvas").off("mouseout");
+    $("#resultsCanvas").on("mouseout", function(e) {
+      view.handleMouseOut(e);
+    });
+    $(window).off("mouseup");
+    $(window).on("mouseup"), function(e) {
+      view.handleGlobalMouseUp(e);
+    }
+    $("#resultsCanvas").off("mousemove");
+    $("#resultsCanvas").on("mousemove", function(e) {
+      view.handleMouseMove(e);
+    });
+  },
+
+  remove: function() {
+    $(window).off("resize");
+    $("#resultsCanvas").off("mousedown");
+    $("#resultsCanvas").off("mouseup");
+    $("#resultsCanvas").off("mousemove");
+    Backbone.View.prototype.remove.apply(this, arguments);
+  },
+
+  handleBeginOrEndChange: function(e, type) {
+    e.preventDefault();
+    var text = $(e.target).val().trim();
+    var d = null;
+    try {
+      d = htrace.parseDate(text);
+    } catch(err) {
+      $("#begin").val(htrace.dateToString(this.begin));
+      $("#end").val(htrace.dateToString(this.end));
+      htrace.showModalWarning("Timeline " + type + " Format Error",
+        "Please enter a valid time in the timeline " + type + " field.<p/>" +
+        err);
+      return null;
+    }
+    if (type === "begin") {
+      this.setBegin(d.valueOf());
+    } else if (type === "end") {
+      this.setEnd(d.valueOf());
+    } else {
+      throw "invalid type for handleBeginOrEndChange: expected begin or end.";
+    }
+  },
+
+  setBegin: function(val) {
+    if (this.end < val + this.MINIMUM_TIME_SPAN) {
+      this.begin = val;
+      this.end = val + this.MINIMUM_TIME_SPAN;
+      console.log("SearchResultsView#setBegin(begin=" + this.begin +
+            ", end=" + this.end + ")");
+      $("#begin").val(htrace.dateToString(this.begin));
+      $("#end").val(htrace.dateToString(this.end));
+    } else {
+      this.begin = val;
+      console.log("SearchResultsView#setBegin(begin=" + this.begin + ")");
+      $("#begin").val(htrace.dateToString(this.begin));
+    }
+    this.render();
+  },
+
+  setEnd: function(val) {
+    if (this.begin + this.MINIMUM_TIME_SPAN > val) {
+      this.begin = val;
+      this.end = this.begin + this.MINIMUM_TIME_SPAN;
+      console.log("SearchResultsView#setEnd(begin=" + this.begin +
+            ", end=" + this.end + ")");
+      $("#begin").val(htrace.dateToString(this.begin));
+      $("#end").val(htrace.dateToString(this.end));
+    } else {
+      this.end = val;
+      console.log("SearchResultsView#setEnd(end=" + this.end + ")");
+      $("#end").val(htrace.dateToString(this.end));
+    }
+    this.render();
+  },
+
+  zoomFitAll: function() {
+    var numSpans = this.model.size();
+    if (numSpans == 0) {
+      this.setBegin(0);
+      this.setEnd(this.MINIMUM_TIME_SPAN);
+      return;
+    }
+    var minStart = 4503599627370496;
+    var maxEnd = 0;
+    for (var i = 0; i < numSpans; i++) {
+      var span = this.model.at(i);
+      if (span.get('begin') < minStart) {
+        minStart = span.get('begin');
+      }
+      if (span.get('end') > maxEnd) {
+        maxEnd = span.get('end');
+      }
+    }
+    this.setBegin(minStart);
+    this.setEnd(maxEnd);
+  }
+});

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/baaa3a52/htrace-htraced/src/web/app/search_view.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/search_view.js b/htrace-htraced/src/web/app/search_view.js
new file mode 100644
index 0000000..52f9101
--- /dev/null
+++ b/htrace-htraced/src/web/app/search_view.js
@@ -0,0 +1,197 @@
+/*
+ * 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: "#results"
+    });
+  },
+
+  events: {
+    "click #searchButton": "searchHandler",
+    "click #clearButton": "clearHandler",
+    "click .add-field": "dropdownHandler",
+    "blur #begin": "blurBeginHandler",
+    "blur #end": "blurEndHandler",
+    "click #zoomButton": "zoomFitAllHandler"
+  },
+
+  searchHandler: function(e){
+    e.preventDefault();
+
+    // Do a new search.
+    this.doSearch(e.ctrlKey);
+  },
+
+  clearHandler: function(e){
+    e.preventDefault();
+
+    // Clear existing search results.
+    this.searchResults.reset();
+  },
+
+  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.zoomFitAll();
+        }
+        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/>");
+          }
+        }
+      },
+      error: function(model, response, options){
+        this.searchResults.clear();
+        var err = "Error " + JSON.stringify(response) +
+          " on span query " + query.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");
+  },
+
+  zoomFitAllHandler: function(e) {
+    e.preventDefault();
+    this.resultsView.zoomFitAll();
+  },
+
+  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/baaa3a52/htrace-htraced/src/web/app/server_info.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/server_info.js b/htrace-htraced/src/web/app/server_info.js
new file mode 100644
index 0000000..b03f706
--- /dev/null
+++ b/htrace-htraced/src/web/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/baaa3a52/htrace-htraced/src/web/app/setup.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/setup.js b/htrace-htraced/src/web/app/setup.js
deleted file mode 100644
index beb06db..0000000
--- a/htrace-htraced/src/web/app/setup.js
+++ /dev/null
@@ -1,192 +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 BaseView = Backbone.Marionette.LayoutView.extend({
-  "el": "body",
-  "regions": {
-    "header": "#header",
-    "app": "#app"
-  }
-});
-
-var Router = Backbone.Marionette.AppRouter.extend({
-  "routes": {
-    "": "init",
-    "!/search(?:query)": "search",
-    "!/spans/:id": "span",
-    "!/swimlane/:id": "swimlane",
-    "!/swimlane/:id:?:lim": "swimlane"
-  },
-
-  "initialize": function() {
-    // Collection
-    this.spansCollection = new app.Spans();
-  },
-
-  "init": function() {
-    Backbone.history.navigate("!/search", {"trigger": true});
-  },
-
-  "search": function(query) {
-    app.root.app.show(new app.SearchView());
-
-    var predicates;
-
-    this.spansCollection.switchMode("infinite", {
-      fetch: false,
-      resetState: true
-    });
-
-    if (query) {
-      predicates = _(query.split(";"))
-      .map(function(predicate) {
-        return _(predicate.split('&'))
-          .reduce(function(mem, op) {
-            var op = op.split('=');
-            mem[op[0]] = op[1];
-            return mem;
-          }, {});
-      });
-      this.spansCollection.fullCollection.reset();
-      this.spansCollection.setPredicates(predicates);
-    }
-    else {
-      this.spansCollection.fullCollection.reset();
-      this.spansCollection.setPredicates([{"op":"cn","field":"description","val":""}]);
-    }
-    this.spansCollection.fetch();
-
-    app.root.app.currentView.controls.show(
-      new app.SearchControlsView({
-        "collection": this.spansCollection,
-        "predicates": predicates
-      }));
-    app.root.app.currentView.main.show(
-      new Backgrid.Grid({
-        "collection": this.spansCollection,
-        "columns": [{
-          "label": "Begin",
-          "cell": Backgrid.Cell.extend({
-            className: "begin-cell",
-            formatter: {
-              fromRaw: function(rawData, model) {
-                var beginMs = model.get("beginTime")
-                return moment(beginMs).format('YYYY/MM/DD HH:mm:ss,SSS');
-              },
-              toRaw: function(formattedData, model) {
-                return formattedData // data entry not supported for this cell
-              }
-            }
-          }),
-          "editable": false,
-          "sortable": false
-        }, {
-          "name": "spanId",
-          "label": "ID",
-          "cell": "string",
-          "editable": false,
-          "sortable": false
-        }, {
-          "name": "processId",
-          "label": "processId",
-          "cell": "string",
-          "editable": false,
-          "sortable": false
-        }, {
-          "label": "Duration",
-          "cell": Backgrid.Cell.extend({
-            className: "duration-cell",
-            formatter: {
-              fromRaw: function(rawData, model) {
-                return model.duration() + " ms"
-              },
-              toRaw: function(formattedData, model) {
-                return formattedData // data entry not supported for this cell
-              }
-            }
-          }),
-          "editable": false,
-          "sortable": false
-        }, {
-          "name": "description",
-          "label": "Description",
-          "cell": "string",
-          "editable": false,
-          "sortable": false
-        }],
-        "row": Backgrid.Row.extend({
-          "events": {
-            "click": "details"
-          },
-          "details": function() {
-            Backbone.history.navigate("!/spans/" + this.model.get("spanId"), {"trigger": true});
-          }
-        })
-      }));
-    app.root.app.currentView.pagination.show(
-      new Backgrid.Extension.Paginator({
-        collection: this.spansCollection,
-      }));
-  },
-
-  "span": function(id) {
-    var span = this.spansCollection.findWhere({
-      "spanId": id
-    });
-
-    if (!span) {
-      Backbone.history.navigate("!/search", {"trigger": true});
-      return;
-    }
-
-    var graphView = new app.GraphView({
-      "collection": this.spansCollection,
-      "id": "span-graph"
-    });
-
-    graphView.on("update:span", function(d) {
-      app.root.app.currentView.span.show(
-        new app.SpanDetailsView({
-          "model": d.span
-        }));
-    });
-
-    app.root.app.show(new app.DetailsView());
-    app.root.app.currentView.content.show(graphView);
-    app.root.app.currentView.content.currentView.setSpanId(id);
-  },
-
-  "swimlane": function(id, lim) {
-    var top = new app.SwimlaneView();
-    app.root.app.show(top);
-    top.swimlane.show(new app.SwimlaneGraphView({
-      "spanId": id,
-      "lim": lim
-    }));
-  }
-});
-
-app.on("start", function(options) {
-  app.root = new BaseView();
-  app.routes = new Router();
-
-  Backbone.history.start();
-});
-
-app.start();

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/baaa3a52/htrace-htraced/src/web/app/span.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/span.js b/htrace-htraced/src/web/app/span.js
new file mode 100644
index 0000000..2c06fa0
--- /dev/null
+++ b/htrace-htraced/src/web/app/span.js
@@ -0,0 +1,69 @@
+/*
+ * 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 = "0000000000000000";
+
+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.s ? response.s : htrace.INVALID_SPAN_ID);
+    this.set("traceId", response.i ? response.i : htrace.INVALID_SPAN_ID);
+    this.set("processId", 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);
+    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.s = this.get("spanId");
+    }
+    if (!(this.get("traceId") === htrace.INVALID_SPAN_ID)) {
+      obj.i = this.get("traceId");
+    }
+    if (!(this.get("processId") === "")) {
+      obj.r = this.get("processId");
+    }
+    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");
+    }
+    return obj;
+  }
+});

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/baaa3a52/htrace-htraced/src/web/app/span_details_view.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/span_details_view.js b/htrace-htraced/src/web/app/span_details_view.js
new file mode 100644
index 0000000..9a37055
--- /dev/null
+++ b/htrace-htraced/src/web/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/baaa3a52/htrace-htraced/src/web/app/span_widget.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/span_widget.js b/htrace-htraced/src/web/app/span_widget.js
new file mode 100644
index 0000000..f9333d6
--- /dev/null
+++ b/htrace-htraced/src/web/app/span_widget.js
@@ -0,0 +1,229 @@
+/*
+ * 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 the trace span displayed on the canvas.
+htrace.SpanWidget = function(params) {
+  for (var k in params) {
+    this[k]=params[k];
+  }
+
+  this.selected = false;
+  this.widgetManagerFocused = false;
+  this.xSize = this.xF - this.x0;
+  this.ySize = this.yF - this.y0;
+  this.xDB = this.xD - this.xB;
+
+  var widgets = [];
+  this.upWidget = new htrace.TriangleButton({
+    ctx: this.ctx,
+    direction: "up",
+    x0: this.xB + 2,
+    xF: this.xB + (this.xDB / 2) - 2,
+    y0: this.y0 + 2,
+    yF: this.yF - 2,
+  });
+  widgets.push(this.upWidget);
+  this.downWidget = new htrace.TriangleButton({
+    ctx: this.ctx,
+    direction: "down",
+    x0: this.xB + (this.xDB / 2) + 2,
+    xF: this.xD - 2,
+    y0: this.y0 + 2,
+    yF: this.yF - 2,
+  });
+  widgets.push(this.downWidget);
+  this.widgetManager = new htrace.WidgetManager({
+    widgets: widgets,
+  });
+
+  this.draw = function() {
+    this.drawBackground();
+    this.drawProcessId();
+    this.drawDescription();
+    this.widgetManager.draw();
+  };
+
+  // Draw the background of this span widget.
+  this.drawBackground = function() {
+    this.ctx.save();
+    if (this.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.drawProcessId = 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('processId'), 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 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.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.inBoundingBox = function(x, y) {
+    return ((x >= this.x0) && (x <= this.xF) && (y >= this.y0) && (y <= this.yF));
+  };
+
+  this.handleMouseDown = function(x, y) {
+    if (!this.inBoundingBox(x, y)) {
+      return false;
+    }
+    if (this.widgetManager.handleMouseDown(x, y)) {
+      this.widgetManagerFocused = true;
+      return true;
+    }
+    this.selected = !this.selected;
+    this.fillSpanDetailsView();
+    return true;
+  };
+
+  this.handleMouseUp = function(x, y) {
+    if (this.widgetManagerFocused) {
+      this.widgetManager.handleMouseUp(x, y);
+      this.widgetManagerFocused = false;
+    }
+  };
+
+  this.handleMouseMove = function(x, y) {
+    if (!this.widgetManagerFocused) {
+      return false;
+    }
+    return this.widgetManager.handleMouseUp(x, y);
+  };
+
+  this.fillSpanDetailsView = function() {
+    var info = {
+      spanID: this.span.get("spanID"),
+      begin: htrace.dateToString(parseInt(this.span.get("begin"), 10)),
+      end: htrace.dateToString(parseInt(this.span.get("end"), 10))
+    };
+    var explicitOrder = {
+      spanId: -3,
+      begin: -2,
+      end: -1
+    };
+    keys = [];
+    for(k in this.span.attributes) {
+      keys.push(k);
+      if (info[k] == null) {
+        info[k] = this.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] || 0;
+        var ob = explicitOrder[b] || 0;
+        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('<tr bgcolor="' + colorString + '">' +
+            '<td style="width:30%;word-wrap:break-word"><%- key %></td>' +
+            '<td style="width:70%;word-wrap:break-word"><%- val %></td>' +
+          "</tr>")({key: keys[i], val: info[keys[i]]});
+    }
+    h += '</table>';
+    $("#spanDetails").html(h);
+  };
+
+  return this;
+};

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/baaa3a52/htrace-htraced/src/web/app/string.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/string.js b/htrace-htraced/src/web/app/string.js
new file mode 100644
index 0000000..b0dfb74
--- /dev/null
+++ b/htrace-htraced/src/web/app/string.js
@@ -0,0 +1,66 @@
+/*
+ * 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
+// (no leading 0x).
+htrace.normalizeSpanId = function(str) {
+  // Strip off the 0x prefix, if there is one.
+  if (str.indexOf("0x") == 0) {
+    str = str.substring(2);
+  }
+  if (str.length != 16) {
+    throw "The length of '" + str + "' was " + str.length +
+      ", but span IDs must be 16 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/baaa3a52/htrace-htraced/src/web/app/time_cursor.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/time_cursor.js b/htrace-htraced/src/web/app/time_cursor.js
new file mode 100644
index 0000000..0060abb
--- /dev/null
+++ b/htrace-htraced/src/web/app/time_cursor.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 || {};
+
+// Draws a vertical bar selecting a time.
+htrace.TimeCursor = function(params) {
+  this.selectedTime = -1;
+  for (var k in params) {
+    this[k]=params[k];
+  }
+
+  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.handleMouseMove = function(x, y) {
+    if ((y >= this.y0) && (y <= this.yF) &&
+        (x >= this.x0) && (x <= this.xF)) {
+      this.selectedTime = this.positionToTime(x);
+      if (this.selectedTime < 0) {
+        $(this.el).val("");
+      } else {
+        $(this.el).val(htrace.dateToString(this.selectedTime));
+      }
+      return true;
+    }
+    return false;
+  };
+
+  return this;
+};

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/baaa3a52/htrace-htraced/src/web/app/triangle_button.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/triangle_button.js b/htrace-htraced/src/web/app/triangle_button.js
new file mode 100644
index 0000000..89f9514
--- /dev/null
+++ b/htrace-htraced/src/web/app/triangle_button.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 || {};
+
+// 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.inBoundingBox = function(x, y) {
+    return ((x >= this.x0) && (x <= this.xF) && (y >= this.y0) && (y <= this.yF));
+  }
+
+  this.handleMouseDown = function(x, y) {
+//    console.log("TriangleButton#handleMouseDown(x=" + x + ", y=" + y +
+//        ", x0=" + this.x0 + ", y0="+ this.y0 +
+//        ", xF=" + this.xF + ", yF=" + this.yF);
+    if (this.inBoundingBox(x,y)) {
+      this.selected = true;
+      return true;
+    }
+    return false;
+  }
+
+  this.handleMouseUp = function(x, y) {
+    if (this.selected) {
+      console.log("executing callback");
+    }
+    this.selected = false;
+  }
+
+  this.handleMouseMove = function(x, y) {
+    var selected = this.inBoundingBox(x,y);
+    if (this.selected != selected) {
+      this.selected = selected;
+      return true;
+    }
+    return false;
+  }
+
+  for (var k in params) {
+    this[k]=params[k];
+  }
+  return this;
+};

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/baaa3a52/htrace-htraced/src/web/app/views/details/details.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/views/details/details.js b/htrace-htraced/src/web/app/views/details/details.js
deleted file mode 100644
index 2f79e1b..0000000
--- a/htrace-htraced/src/web/app/views/details/details.js
+++ /dev/null
@@ -1,47 +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.
- */
-
-app.DetailsView = Backbone.Marionette.LayoutView.extend({
-  "template": "#details-layout-template",
-  "regions": {
-    "span": "div[role='complementary']",
-    "content": "div[role='main']"
-  }
-});
-
-app.SpanDetailsView = Backbone.Marionette.ItemView.extend({
-  "className": "span",
-  "template": "#span-details-template",
-
-  "serializeData": function() {
-    var context = {
-      "span": this.model.toJSON()
-    };
-    context["span"]["duration"] = this.model.duration();
-    return context;
-  },
-  
-  "events": {
-    "click": "swimlane"
-  },
-  "swimlane": function() {
-    Backbone.history.navigate("!/swimlane/" + this.model.get("spanId"),
-                              {"trigger": true});
-  }
-});

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/baaa3a52/htrace-htraced/src/web/app/views/graph/graph.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/views/graph/graph.js b/htrace-htraced/src/web/app/views/graph/graph.js
deleted file mode 100644
index 7b4f89e..0000000
--- a/htrace-htraced/src/web/app/views/graph/graph.js
+++ /dev/null
@@ -1,262 +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.
- */
-
-app.GraphView = Backbone.View.extend({
-  initialize: function(options) {
-    options = options || {};
-
-    if (!options.id) {
-      console.error("GraphView requires argument 'id' to uniquely identify this graph.");
-      return;
-    }
-
-    _.bindAll(this, "render");
-    this.collection.bind('change', this.render);
-
-    var links = this.links = [];
-    var linkTable = this.linkTable = {};
-    var nodes = this.nodes = [];
-    var nodeTable = this.nodeTable = {};
-    var force = this.force
-        = d3.layout.force().size([$(window).width(), $(window).height() * 3/4])
-                           .linkDistance($(window).height() / 5)
-                           .charge(-120)
-                           .gravity(0)
-                           ;
-    force.nodes(nodes)
-         .links(links);
-
-    force.on("tick", function(e) {
-      var root = d3.select("#" + options.id);
-      
-      if (!root.node()) {
-        return;
-      }
-
-      var selectedDatum = root.select(".selected").datum();
-
-      // center selected node
-      root.select("svg").attr("width", $(root.node()).width());
-      selectedDatum.x = root.select("svg").attr("width") / 2;
-      selectedDatum.y = root.select("svg").attr("height") / 2;
-
-      // Push sources up and targets down to form a weak tree.
-      var k = 10 * e.alpha;
-      force.links().forEach(function(d, i) {
-        d.source.y -= k;
-        d.target.y += k;
-      });
-
-      var nodes = root.selectAll(".node").data(force.nodes());
-      nodes.select("circle")
-        .attr("cx", function(d) { return d.x; })
-        .attr("cy", function(d) { return d.y; });
-      nodes.select("text")
-        .attr("x", function(d) { return d.x - this.getComputedTextLength() / 2; })
-        .attr("y", function(d) { return d.y; });
-      root.selectAll(".link").data(force.links())
-        .attr("d", function(d) {
-          var start = {},
-              end = {},
-              angle = Math.atan2((d.target.x - d.source.x), (d.target.y - d.source.y));
-          start.x = d.source.x + d.source.r * Math.sin(angle);
-          end.x = d.target.x - d.source.r * Math.sin(angle);
-          start.y = d.source.y + d.source.r * Math.cos(angle);
-          end.y = d.target.y - d.source.r * Math.cos(angle);
-          return "M" + start.x + " " + start.y
-              + " L" + end.x + " " + end.y;
-        });
-    });
-  },
-
-  updateLinksAndNodes: function() {
-    if (!this.spanId) {
-      return;
-    }
-
-    var $this = this, collection = this.collection;
-
-    var selectedSpan = this.collection.findWhere({
-      "spanId": this.spanId
-    });
-
-    var findChildren = function(span) {
-      var spanId = span.get("spanId");
-      var spans = collection.filter(function(model) {
-        return _(model.get("parents")).contains(spanId);
-      });
-      return _(spans).reject(function(span) {
-        return span == null;
-      });
-    };
-    var findParents = function(span) {
-      var spans = _(span.get("parents")).map(function(parentSpanId) {
-        return collection.findWhere({
-          "spanId": parentSpanId
-        });
-      });
-      return _(spans).reject(function(span) {
-        return span == null;
-      });
-    };
-    var spanToNode = function(span, level) {
-      var table = $this.nodeTable;
-      if (!(span.get("spanId") in table)) {
-        table[span.get("spanId")] = {
-          "name": span.get("spanId"),
-          "span": span,
-          "level": level,
-          "group": 0,
-          "x": parseInt($this.svg.attr('width')) / 2,
-          "y": 250 + level * 50
-        };
-        $this.nodes.push(table[span.get("spanId")]);
-      }
-
-      return table[span.get("spanId")];
-    };
-    var createLink = function(source, target) {
-      var table = $this.linkTable;
-      var name = source.span.get("spanId") + "-" + target.span.get("spanId");
-      if (!(name in table)) {
-        table[name] = {
-          "source": source,
-          "target": target
-        };
-        $this.links.push(table[name]);
-      }
-
-      return table[name];
-    };
-
-    var parents = [], children = [];
-    var selectedSpanNode = spanToNode(selectedSpan, 1);
-
-    Array.prototype.push.apply(parents, findParents(selectedSpan));
-    _(parents).each(function(span) {
-      Array.prototype.push.apply(parents, findParents(span));
-      createLink(spanToNode(span, 0), selectedSpanNode)
-    });
-
-    Array.prototype.push.apply(children, findChildren(selectedSpan));
-    _(children).each(function(span) {
-      Array.prototype.push.apply(children, findChildren(span));
-      createLink(selectedSpanNode, spanToNode(span, 2))
-    });
-  },
-
-  renderLinks: function(selection) {
-    var path = selection.enter().append("path")
-        .classed("link", true)
-        .style("marker-end",  "url(#suit)");
-    selection.exit().remove();
-    return selection;
-  },
-
-  renderNodes: function(selection) {
-    var $this = this;
-    var g = selection.enter().append("g").attr("class", "node");
-    var circle = g.append("circle")
-      .attr("r", function(d) {
-        if (!d.radius) {
-          d.r = Math.log(d.span.duration());
-         
-          if (d.r > app.GraphView.MAX_NODE_SIZE) {
-            d.r = app.GraphView.MAX_NODE_SIZE;
-          }
-
-          if (d.r < app.GraphView.MIN_NODE_SIZE) {
-            d.r = app.GraphView.MIN_NODE_SIZE;
-          }
-        }
-
-        return d.r;
-      });
-    var text = g.append("text").text(function(d) {
-      return d.span.get("description");
-    });
-
-    selection.exit().remove();
-
-    circle.on("click", function(d) {
-      $this.setSpanId(d.name);
-    });
-
-    selection.classed("selected", null);
-    selection.filter(function(d) {
-      return d.span.get("spanId") == $this.spanId;
-    }).classed("selected", true);
-    
-    return selection;
-  },
-
-  setSpanId: function(spanId) {
-    var $this = this;
-    this.spanId = spanId;
-
-    this.updateLinksAndNodes();
-
-    this.renderNodes(
-      this.svg.selectAll(".node")
-        .data(this.force.nodes(), function(d) {
-          return d.name;
-        }));
-
-    this.renderLinks(
-      this.svg.selectAll(".link")
-        .data(this.force.links(), function(d) {
-          return d.source.name + "-" + d.target.name;
-        }));
-
-    this.force.start();
-
-    Backbone.history.navigate("!/spans/" + spanId);
-    this.trigger("update:span", {"span": this.collection.findWhere({
-      "spanId": spanId
-    })});
-  },
-
-  render: function() {
-    this.svg = d3.select(this.$el[0]).append("svg");
-    this.svg.attr("height", 500)
-       .attr("width", $(window).width())
-       .attr("id", this.id);
-
-    // Arrows
-    this.svg.append("defs").selectAll("marker")
-      .data(["suit", "licensing", "resolved"])
-    .enter().append("marker")
-      .attr("id", function(d) { return d; })
-      .attr("viewBox", "0 -5 10 10")
-      .attr("refX", 25)
-      .attr("refY", 0)
-      .attr("markerWidth", 6)
-      .attr("markerHeight", 6)
-      .attr("orient", "auto")
-    .append("path")
-      .attr("d", "M0,-5L10,0L0,5 L10,0 L0, -5")
-      .style("stroke", "#4679BD")
-      .style("opacity", "0.6");
-
-    return this;
-  }
-});
-
-app.GraphView.MAX_NODE_SIZE = 150;
-app.GraphView.MIN_NODE_SIZE = 50;