HTRACE-194. gui: support multiple selections, zooming to fit a group of spans, deleting a group of spans (Colin Patrick McCabe via iwasakims)
diff --git a/htrace-webapp/src/main/web/app/search_results_view.js b/htrace-webapp/src/main/web/app/search_results_view.js
index f7ece4b..3f009d4 100644
--- a/htrace-webapp/src/main/web/app/search_results_view.js
+++ b/htrace-webapp/src/main/web/app/search_results_view.js
@@ -60,7 +60,8 @@
this.widgetManager.handle({
type: "mouseDown",
x: this.getCanvasX(e),
- y: this.getCanvasY(e)
+ y: this.getCanvasY(e),
+ raw: e
});
this.draw();
},
@@ -70,7 +71,8 @@
this.widgetManager.handle({
type: "mouseUp",
x: this.getCanvasX(e),
- y: this.getCanvasY(e)
+ y: this.getCanvasY(e),
+ raw: e
});
this.draw();
},
@@ -88,7 +90,8 @@
this.widgetManager.handle({
type: "mouseMove",
x: this.getCanvasX(e),
- y: this.getCanvasY(e)
+ y: this.getCanvasY(e),
+ raw: e
});
this.draw();
},
@@ -256,6 +259,10 @@
$("#resultsCanvas").on("mousemove", function(e) {
view.handleMouseMove(e);
});
+ $("#resultsCanvas").off("contextmenu");
+ $("#resultsCanvas").on("contextmenu", function(e) {
+ return false;
+ });
},
remove: function() {
@@ -281,68 +288,117 @@
return null;
}
if (type === "begin") {
- this.setBegin(d.valueOf());
+ this.setTimes({begin: d.valueOf()});
} else if (type === "end") {
- this.setEnd(d.valueOf());
+ this.setTimes({end: 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));
+ setTimes: function(params) {
+ if (params["begin"]) {
+ this.begin = params["begin"];
}
- this.render();
+ if (params["end"]) {
+ this.end = params["end"];
+ }
+ if (this.end < this.begin) {
+ var b = this.begin;
+ this.begin = this.end;
+ this.end = b;
+ }
+ var delta = this.end - this.begin;
+ if (delta < this.MINIMUM_TIME_SPAN) {
+ var needed = this.MINIMUM_TIME_SPAN - delta;
+ this.begin -= (needed / 2);
+ this.end += (needed / 2);
+ }
+ $("#begin").val(htrace.dateToString(this.begin));
+ $("#end").val(htrace.dateToString(this.end));
+ // caller should invoke render()
},
- zoomFitAll: function() {
- var numResults = this.searchResults.size();
+ clearHandler: function() {
+ console.log("invoking clearHandler.");
+ var toDelete = []
+ var noneSelected = true;
+ for (var i = 0; i < this.searchResults.length; i++) {
+ var resultSelected = false;
+ var model = this.searchResults.at(i);
+ htrace.treeTraverseDepthFirstPre(model,
+ htrace.getReifiedChildren, 0,
+ function(node, depth) {
+ if (noneSelected) {
+ if (node.get("selected")) {
+ resultSelected = true;
+ }
+ }
+ });
+ htrace.treeTraverseDepthFirstPre(model,
+ htrace.getReifiedParents, 0,
+ function(node, depth) {
+ if (node.get("selected")) {
+ resultSelected = true;
+ }
+ });
+ if (resultSelected) {
+ if (noneSelected) {
+ toDelete = [];
+ noneSelected = false;
+ }
+ toDelete.push(model);
+ } else if (noneSelected) {
+ toDelete.push(model);
+ }
+ }
+ this.render();
+ console.log("clearHandler: removing " + JSON.stringify(toDelete));
+ this.searchResults.remove(toDelete);
+ },
+
+ getSelectedSpansOrAllSpans: function() {
+ // Get the list of selected spans.
+ // If there are no spans selected, we return all spans.
+ var ret = [];
+ var noneSelected = true;
+ this.applyToAllSpans(function(span) {
+ if (span.get("selected")) {
+ if (noneSelected) {
+ ret = [];
+ noneSelected = false;
+ }
+ ret.push(span);
+ } else if (noneSelected) {
+ ret.push(span);
+ }
+ });
+ return ret;
+ },
+
+ zoomHandler: function() {
+ var zoomSpans = this.getSelectedSpansOrAllSpans();
+ var numResults = zoomSpans.length;
if (numResults == 0) {
- this.setBegin(0);
- this.setEnd(this.MINIMUM_TIME_SPAN);
+ this.setTimes({begin:0, end:this.MINIMUM_TIME_SPAN});
+ this.render();
return;
}
var minStart = 4503599627370496;
var maxEnd = 0;
for (var i = 0; i < numResults; i++) {
- var span = this.searchResults.at(i);
- var begin = span.getEarliestBegin();
+ var begin = zoomSpans[i].getEarliestBegin();
if (begin < minStart) {
minStart = begin;
}
- var end = span.getLatestEnd();
- if (end > minStart) {
+ var end = zoomSpans[i].getLatestEnd();
+ if (end > maxEnd) {
maxEnd = end;
}
}
- this.setBegin(minStart);
- this.setEnd(maxEnd);
+ this.setTimes({begin: minStart, end: maxEnd});
+ this.render();
},
// Apply a function to all spans
@@ -351,7 +407,6 @@
htrace.treeTraverseDepthFirstPre(this.searchResults.at(i),
htrace.getReifiedChildren, 0,
function(node, depth) {
- console.log("node = " + node + ", node.constructor.name = " + node.constructor.name);
cb(node);
});
htrace.treeTraverseDepthFirstPre(this.searchResults.at(i),
diff --git a/htrace-webapp/src/main/web/app/search_view.js b/htrace-webapp/src/main/web/app/search_view.js
index 5144650..a530d70 100644
--- a/htrace-webapp/src/main/web/app/search_view.js
+++ b/htrace-webapp/src/main/web/app/search_view.js
@@ -36,21 +36,19 @@
"click .add-field": "dropdownHandler",
"blur #begin": "blurBeginHandler",
"blur #end": "blurEndHandler",
- "click #zoomButton": "zoomFitAllHandler"
+ "click #zoomButton": "zoomHandler"
},
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();
+ this.resultsView.clearHandler();
},
doSearch: function(showDebug){
@@ -105,7 +103,7 @@
if (firstResults) {
// After the initial search, zoom to fit everything.
// On subsequent searches, we leave the viewport alone.
- searchView.resultsView.zoomFitAll();
+ searchView.resultsView.zoomHandler();
}
searchView.searchInProgress = false;
if (showDebug) {
@@ -168,9 +166,9 @@
return this.resultsView.handleBeginOrEndChange(e, "end");
},
- zoomFitAllHandler: function(e) {
+ zoomHandler: function(e) {
e.preventDefault();
- this.resultsView.zoomFitAll();
+ this.resultsView.zoomHandler();
},
removePredicateView: function(predicateView) {
diff --git a/htrace-webapp/src/main/web/app/span_widget.js b/htrace-webapp/src/main/web/app/span_widget.js
index ad5ea07..e06c8b4 100644
--- a/htrace-webapp/src/main/web/app/span_widget.js
+++ b/htrace-webapp/src/main/web/app/span_widget.js
@@ -19,6 +19,94 @@
var htrace = htrace || {};
+htrace.fillSpanDetailsView = 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('<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);
+};
+
+htrace.clearSpanDetailsView = function() {
+ $("#spanDetails").html("");
+};
+
// Widget containing the trace span displayed on the canvas.
htrace.SpanWidget = function(params) {
this.draw = function() {
@@ -113,90 +201,6 @@
(this.end - this.begin));
};
- this.fillSpanDetailsView = function() {
- var info = {
- spanID: this.span.get("spanID"),
- begin: htrace.dateToString(this.span.get("begin"), 10),
- end: htrace.dateToString(this.span.get("end"), 10),
- duration: ((this.span.get("end") - this.span.get("begin")) + " ms")
- };
- var explicitOrder = {
- spanId: 1,
- begin: 2,
- end: 3,
- duration: 4
- };
- keys = ["duration"];
- for(k in this.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 = this.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 = this.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] = 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] || 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('<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);
- };
-
this.handle = function(e) {
switch (e.type) {
case "mouseDown":
@@ -204,13 +208,42 @@
this.x0, this.xF, this.y0, this.yF)) {
return true;
}
- this.manager.searchResultsView.applyToAllSpans(function(span) {
- if (span.get("selected") == true) {
- span.set("selected", false);
- }
- });
- this.span.set("selected", true);
- this.fillSpanDetailsView();
+ if (e.raw.ctrlKey) {
+ // If the control key is pressed, we can unselect the current
+ // selection, or create multiple selections.
+ if (this.span.get("selected")) {
+ this.span.set("selected", false);
+ } else {
+ this.span.set("selected", true);
+ }
+ var selection = null;
+ var multipleSelections = false;
+ this.manager.searchResultsView.applyToAllSpans(function(span) {
+ if (span.get("selected")) {
+ if (selection == null) {
+ selection = span;
+ } else {
+ multipleSelections = true;
+ }
+ }
+ });
+ if (multipleSelections) {
+ selection = null;
+ }
+ if (selection == null) {
+ htrace.clearSpanDetailsView();
+ } else {
+ htrace.fillSpanDetailsView(selection);
+ }
+ } else {
+ this.manager.searchResultsView.applyToAllSpans(function(span) {
+ if (span.get("selected")) {
+ span.set("selected", false);
+ }
+ });
+ this.span.set("selected", true);
+ htrace.fillSpanDetailsView(this.span);
+ }
return true;
case "draw":
this.draw();