diff --git a/static/css/quay.css b/static/css/quay.css
index dac9aa59a..6f016dd56 100644
--- a/static/css/quay.css
+++ b/static/css/quay.css
@@ -717,6 +717,10 @@ p.editable:hover i {
margin-bottom: 12px;
}
+#image-history-container {
+ overflow: hidden;
+}
+
#image-history-container .node circle {
cursor: pointer;
fill: #fff;
@@ -739,6 +743,10 @@ p.editable:hover i {
font-weight: bold;
}
+#image-history-container .node text.collapsed {
+ fill: gray;
+}
+
#image-history-container .node text {
font-size: 15px;
cursor: pointer;
@@ -873,6 +881,7 @@ p.editable:hover i {
color: #fff;
border-radius: 2px;
max-width: 500px;
+ z-index: 9999999;
}
/* Creates a small triangle extender for the tooltip */
diff --git a/static/js/graphing.js b/static/js/graphing.js
index 84f119bf3..000abfbdb 100644
--- a/static/js/graphing.js
+++ b/static/js/graphing.js
@@ -1,4 +1,5 @@
var DEPTH_HEIGHT = 100;
+var DEPTH_WIDTH = 132;
/**
* Based off of http://mbostock.github.io/d3/talk/20111018/tree.html by Mike Bostock (@mbostock)
@@ -51,19 +52,32 @@ function ImageHistoryTree(namespace, name, images, current, formatComment, forma
*/
ImageHistoryTree.prototype.draw = function(container) {
// Build the root of the tree.
- this.maxHeight_ = this.buildRoot_();
+ var result = this.buildRoot_();
+ this.maxWidth_ = result['maxWidth'];
+ this.maxHeight_ = result['maxHeight'];
// Determine the size of the SVG container.
- var width = document.getElementById(container).clientWidth;
- var height = Math.max(width * 0.625, this.maxHeight_ * (DEPTH_HEIGHT + 10));
+ var width = Math.max(document.getElementById(container).clientWidth, this.maxWidth_ * DEPTH_WIDTH);
+ var height = this.maxHeight_ * (DEPTH_HEIGHT + 10);
- var margin = { top: 40, right: 20, bottom: 20, left: 20 };
+ // Set the height of the container so that it never goes offscreen.
+ var viewportHeight = $(window).height();
+ var boundingBox = document.getElementById(container).getBoundingClientRect();
+ document.getElementById(container).style.maxHeight = (viewportHeight - boundingBox.top) + 'px';
+
+ var leftMargin = 20;
+ if (width > document.getElementById(container).clientWidth) {
+ leftMargin = 120;
+ }
+
+ var margin = { top: 40, right: 20, bottom: 20, left: leftMargin };
var m = [margin.top, margin.right, margin.bottom, margin.left];
var w = width - m[1] - m[3];
var h = height - m[0] - m[2];
// Create the tree and all its components.
var tree = d3.layout.tree()
+ .separation(function() { return 2; })
.size([w, h]);
var diagonal = d3.svg.diagonal()
@@ -83,6 +97,14 @@ ImageHistoryTree.prototype.draw = function(container) {
.direction('e')
.html(function(d) {
var html = '';
+ if (d.collapsed) {
+ for (var i = 0; i < d.encountered.length; ++i) {
+ html += '' + d.encountered[i].image.id.substr(0, 12) + '';
+ html += '' + formatTime(d.encountered[i].image.created) + '';
+ }
+ return html;
+ }
+
if (d.image.comment) {
html += '';
}
@@ -106,7 +128,11 @@ ImageHistoryTree.prototype.draw = function(container) {
this.tree_ = tree;
// Populate the tree.
- this.populate_();
+ this.root_.x0 = this.fullWidth_ / 2;
+ this.root_.y0 = 0;
+ this.setTag_(this.currentTag_);
+
+ $('#' + container).overscroll();
};
@@ -167,18 +193,6 @@ ImageHistoryTree.prototype.changeImage_ = function(imageId) {
};
-/**
- * Marks the image node with whether it is the current image and/or highlighted.
- */
-ImageHistoryTree.prototype.markWithState_ = function(image, imageNode) {
- var currentAncestors = this.getAncestors_(this.currentImage_);
- var isCurrent = image.id == this.currentImage_.id;
- var isHighlighted = currentAncestors.indexOf(image.dbid.toString()) >= 0;
- imageNode.highlighted = isHighlighted || isCurrent;
- imageNode.current = isCurrent;
-};
-
-
/**
* Builds the root node for the tree.
*/
@@ -200,37 +214,121 @@ ImageHistoryTree.prototype.buildRoot_ = function() {
"image": image,
"tags": image.tags
};
- this.markWithState_(image, imageNode);
imageByDBID[image.dbid] = imageNode;
}
this.imageByDBID_ = imageByDBID;
// For each node, attach it to its immediate parent. If there is no immediate parent,
// then the node is the root.
- var maxAncestorCount = 0;
for (var i = 0; i < this.images_.length; ++i) {
var image = this.images_[i];
var imageNode = imageByDBID[image.dbid];
var ancestors = this.getAncestors_(image);
var immediateParent = ancestors[ancestors.length - 1] * 1;
var parent = imageByDBID[immediateParent];
- if (parent) {
- // If the image node is highlighted, ensure it is at the front
- // of the child list.
- if (imageNode.highlighted) {
- parent.children.splice(0, 0, imageNode);
- } else {
- parent.children.push(imageNode);
- }
+ if (parent) {
+ // Add a reference to the parent. This makes walking the tree later easier.
+ imageNode.parent = parent;
+ parent.children.push(imageNode);
} else {
formatted = imageNode;
}
-
- maxAncestorCount = Math.max(maxAncestorCount, ancestors.length);
}
+ // Determine the maximum number of nodes at a particular level. This is used to size
+ // the width of the tree properly.
+ var maxChildCount = 0;
+ for (var i = 0; i < this.images_.length; ++i) {
+ var image = this.images_[i];
+ var imageNode = imageByDBID[image.dbid];
+ maxChildCount = Math.max(maxChildCount, this.determineMaximumChildCount_(imageNode));
+ }
+
+ // Compact the graph so that any single chain of three (or more) images becomes a collapsed
+ // section. We only do this if the max width is > 1 (since for a single width tree, no long
+ // chain will hide a branch).
+ if (maxChildCount > 1) {
+ this.collapseNodes_(formatted);
+ }
+
+ // Determine the maximum height of the tree.
+ var maxHeight = this.determineMaximumHeight_(formatted);
+
+ // Finally, set the root node and return.
this.root_ = formatted;
- return maxAncestorCount + 1;
+
+ return {
+ 'maxWidth': maxChildCount + 1,
+ 'maxHeight': maxHeight
+ };
+};
+
+
+/**
+ * Collapses long single chains of nodes (3 or more) into single nodes to make the graph more
+ * compact.
+ */
+ImageHistoryTree.prototype.determineMaximumHeight_ = function(node) {
+ var maxHeight = 0;
+ for (var i = 0; i < node.children.length; ++i) {
+ maxHeight = Math.max(this.determineMaximumHeight_(node.children[i]), maxHeight);
+ }
+ return maxHeight + 1;
+};
+
+
+/**
+ * Collapses long single chains of nodes (3 or more) into single nodes to make the graph more
+ * compact.
+ */
+ImageHistoryTree.prototype.collapseNodes_ = function(node) {
+ if (node.children.length == 1) {
+ // Keep searching downward until we find a node with more than a single child.
+ var current = node;
+ var previous = node;
+ var encountered = [];
+ while (current.children.length == 1) {
+ encountered.push(current);
+ previous = current;
+ current = current.children[0];
+ }
+
+ if (encountered.length >= 3) {
+ // Collapse the node.
+ var collapsed = {
+ "name": '(' + encountered.length + ' images)',
+ "children": [current],
+ "collapsed": true,
+ "encountered": encountered
+ };
+ node.children = [collapsed];
+
+ // Update the parent relationships.
+ collapsed.parent = node;
+ current.parent = collapsed;
+ return;
+ }
+ }
+
+ for (var i = 0; i < node.children.length; ++i) {
+ this.collapseNodes_(node.children[i]);
+ }
+};
+
+
+/**
+ * Determines the maximum child count for the node and its children.
+ */
+ImageHistoryTree.prototype.determineMaximumChildCount_ = function(node) {
+ var children = node.children;
+ var myLevelCount = children.length;
+ var nestedCount = 0;
+
+ for (var i = 0; i < children.length; ++i) {
+ nestedCount += children[i].children.length;
+ }
+
+ return Math.max(myLevelCount, nestedCount);
};
@@ -250,25 +348,38 @@ ImageHistoryTree.prototype.findImage_ = function(checker) {
};
+/**
+ * Marks the full node path from the given starting node on whether it is highlighted.
+ */
+ImageHistoryTree.prototype.markPath_ = function(startingNode, isHighlighted) {
+ var currentNode = startingNode;
+ currentNode.current = isHighlighted;
+ while (currentNode != null) {
+ currentNode.highlighted = isHighlighted;
+ currentNode = currentNode.parent;
+ }
+};
+
+
/**
* Sets the current tag displayed in the tree.
*/
ImageHistoryTree.prototype.setTag_ = function(tagName) {
this.currentTag_ = tagName;
+
+ // Update the state of each existing node to no longer be highlighted.
+ var imageByDBID = this.imageByDBID_;
+ var currentNode = imageByDBID[this.currentImage_.dbid];
+ this.markPath_(currentNode, false);
// Find the new current image.
this.currentImage_ = this.findImage_(function(image) {
return image.tags.indexOf(tagName) >= 0;
});
- // Update the state of each node.
- var imageByDBID = this.imageByDBID_;
- var currentAncestors = this.getAncestors_(this.currentImage_);
- for (var i = 0; i < this.images_.length; ++i) {
- var image = this.images_[i];
- var imageNode = this.imageByDBID_[image.dbid];
- this.markWithState_(image, imageNode);
- }
+ // Update the state of the new node path.
+ currentNode = imageByDBID[this.currentImage_.dbid];
+ this.markPath_(currentNode, true);
// Ensure that the children are in the correct order.
for (var i = 0; i < this.images_.length; ++i) {
@@ -285,12 +396,15 @@ ImageHistoryTree.prototype.setTag_ = function(tagName) {
if (arr[0] != imageNode) {
var index = arr.indexOf(imageNode);
- arr.splice(index, 1);
- arr.splice(0, 0, imageNode);
+ if (index > 0) {
+ arr.splice(index, 1);
+ arr.splice(0, 0, imageNode);
+ }
}
}
}
+ // Update the tree.
this.update_(this.root_);
};
@@ -313,19 +427,6 @@ ImageHistoryTree.prototype.setImage_ = function(imageId) {
};
-/**
- * Populates the tree.
- */
-ImageHistoryTree.prototype.populate_ = function() {
- // Set the position of the initial node.
- this.root_.x0 = this.fullWidth_ / 2;
- this.root_.y0 = 0;
-
- // Initialize the display to show the current path of nodes.
- this.update_(this.root_);
-};
-
-
/**
* Updates the tree in response to a click on a node.
*/
@@ -420,6 +521,9 @@ ImageHistoryTree.prototype.update_ = function(source) {
// Update the repo text.
nodeUpdate.select("text")
.attr("class", function(d) {
+ if (d.collapsed) {
+ return 'collapsed';
+ }
return d.image.id == that.currentImage_.id ? 'current' : '';
});
@@ -430,6 +534,10 @@ ImageHistoryTree.prototype.update_ = function(source) {
// Update the tags.
node.select(".tags")
.html(function(d) {
+ if (!d.tags) {
+ return '';
+ }
+
var html = '';
for (var i = 0; i < d.tags.length; ++i) {
var tag = d.tags[i];
diff --git a/static/lib/jquery.overscroll.min.js b/static/lib/jquery.overscroll.min.js
new file mode 100644
index 000000000..e693ecc1a
--- /dev/null
+++ b/static/lib/jquery.overscroll.min.js
@@ -0,0 +1 @@
+!function(a,b,c,d,e,f,g,h,i){"use strict";function j(a,b){b.trigger("overscroll:"+a)}function k(){return(new Date).getTime()}function l(a,b,c){return b.x=a.pageX,b.y=a.pageY,b.time=k(),b.index=c,b}function m(a,b,c,d){var e,f;a&&a.added&&(a.horizontal&&(e=c*(1+b.container.width/b.container.scrollWidth),f=d+b.thumbs.horizontal.top,a.horizontal.css("margin",f+"px 0 0 "+e+"px")),a.vertical&&(e=c+b.thumbs.vertical.left,f=d*(1+b.container.height/b.container.scrollHeight),a.vertical.css("margin",f+"px 0 0 "+e+"px")))}function n(a,b,c){a&&a.added&&!b.persistThumbs&&(c?(a.vertical&&a.vertical.stop(!0,!0).fadeTo("fast",J.thumbOpacity),a.horizontal&&a.horizontal.stop(!0,!0).fadeTo("fast",J.thumbOpacity)):(a.vertical&&a.vertical.fadeTo("fast",0),a.horizontal&&a.horizontal.fadeTo("fast",0)))}function o(a){var b,c="events",d=h._data?h._data(a[0],c):a.data(c);d&&d.click&&(b=d.click.slice(),a.off("click").one("click",function(){return h.each(b,function(b,c){a.click(c)}),!1}))}function p(a){var b=a.data,c=b.thumbs,d=b.options,e="mouseenter"===a.type;n(c,d,e)}function q(a){var b=a.data;b.flags.dragged||m(b.thumbs,b.sizing,this.scrollLeft,this.scrollTop)}function r(a){a.preventDefault();var b=a.data,c=b.options,d=b.sizing,g=b.thumbs,h=b.wheel,i=b.flags,j=a.originalEvent,k=0,l=0,o=0;i.drifting=!1,j.detail?(k=-j.detail,j.detailX&&(l=-j.detailX),j.detailY&&(o=-j.detailY)):j.wheelDelta&&(k=j.wheelDelta/J.wheelTicks,j.wheelDeltaX&&(l=j.wheelDeltaX/J.wheelTicks),j.wheelDeltaY&&(o=j.wheelDeltaY/J.wheelTicks)),k*=c.wheelDelta,l*=c.wheelDelta,o*=c.wheelDelta,h||(b.target.data(G).dragging=i.dragging=!0,b.wheel=h={timeout:null},n(g,c,!0)),"vertical"===c.wheelDirection?this.scrollTop-=k:"horizontal"===c.wheelDirection?this.scrollLeft-=k:(this.scrollLeft-=l,this.scrollTop-=o||k),h.timeout&&f(h.timeout),m(g,d,this.scrollLeft,this.scrollTop),h.timeout=e(function(){b.target.data(G).dragging=i.dragging=!1,n(g,c,b.wheel=null)},J.thumbTimeout)}function s(a){a.preventDefault();var b=a.data,c=a.originalEvent.touches,d=b.options,e=b.sizing,f=b.thumbs,g=b.position,h=b.flags,i=b.target.get(0);c&&c.length&&(a=c[0]),h.dragged||n(f,d,!0),h.dragged=!0,"vertical"!==d.direction&&(i.scrollLeft-=a.pageX-g.x),"horizontal"!==b.options.direction&&(i.scrollTop-=a.pageY-g.y),l(a,b.position),--b.capture.index<=0&&(b.target.data(G).dragging=h.dragging=!0,l(a,b.capture,J.captureThreshold)),m(f,e,i.scrollLeft,i.scrollTop)}function t(a,b,c){var d,e,f,g,h=b.data,i=h.capture,l=h.options,n=h.sizing,o=h.thumbs,p=k()-i.time,q=a.scrollLeft,r=a.scrollTop,s=J.driftDecay;return p>J.driftTimeout?(c(h),void 0):(d=l.scrollDelta*(b.pageX-i.x),e=l.scrollDelta*(b.pageY-i.y),"vertical"!==l.direction&&(q-=d),"horizontal"!==l.direction&&(r-=e),f=d/J.driftSequences,g=e/J.driftSequences,j("driftstart",h.target),h.drifting=!0,H.animate(function t(){if(h.drifting){var b=1,d=-1;h.drifting=!1,(g>b&&a.scrollTop>r||d>g&&a.scrollTopb&&a.scrollLeft>q||d>f&&a.scrollLeft=a.scrollWidth?c:a.scrollWidth,f=d>=a.scrollHeight?d:a.scrollHeight,g=e>c||f>d;return{valid:g,container:{width:c,height:d,scrollWidth:e,scrollHeight:f},thumbs:{horizontal:{width:c*c/e,height:J.thumbThickness,corner:J.thumbThickness/2,left:0,top:d-J.thumbThickness},vertical:{width:J.thumbThickness,height:d*d/f,corner:J.thumbThickness/2,left:c-J.thumbThickness,top:0}}}}function y(a,b){var c,d=h(a),e=d.data(G)||{},f=d.attr("style"),g=b?function(){e=d.data(G),c=e.thumbs,f?d.attr("style",f):d.removeAttr("style"),c&&(c.horizontal&&c.horizontal.remove(),c.vertical&&c.vertical.remove()),d.removeData(G).off(I.wheel,r).off(I.start,u).off(I.end,v).off(I.ignored,B)}:h.noop;return h.isFunction(e.remover)?e.remover:g}function z(a,b){return{position:"absolute",opacity:b.persistThumbs?J.thumbOpacity:0,"background-color":"black",width:a.width+"px",height:a.height+"px","border-radius":a.corner+"px",margin:a.top+"px 0 0 "+a.left+"px","z-index":b.zIndex}}function A(a,b,c){var d="",e={},f=!1;return b.container.scrollWidth>0&&"vertical"!==c.direction&&(f=z(b.thumbs.horizontal,c),e.horizontal=h(d).css(f).prependTo(a)),b.container.scrollHeight>0&&"horizontal"!==c.direction&&(f=z(b.thumbs.vertical,c),e.vertical=h(d).css(f).prependTo(a)),e.added=!!f,e}function B(a){a.preventDefault()}function C(a,b){b=w(b);var c,d=x(a),e={options:b,sizing:d,flags:{dragging:!1},remover:y(a,!0)};(d.valid||b.ignoreSizing)&&(e.target=a=h(a).css({position:"relative",cursor:H.cursor.grab}).on(I.start,e,u).on(I.end,e,v).on(I.ignored,e,B),b.dragHold?h(document).on(I.end,e,v):e.target.on(I.end,e,v),null!==b.scrollLeft&&a.scrollLeft(b.scrollLeft),null!==b.scrollTop&&a.scrollTop(b.scrollTop),H.overflowScrolling?a.css(H.overflowScrolling,"touch"):a.on(I.scroll,e,q),b.captureWheel&&a.on(I.wheel,e,r),b.showThumbs?H.overflowScrolling?a.css("overflow","scroll"):(a.css("overflow","hidden"),e.thumbs=c=A(a,d,b),c.added&&(m(c,d,a.scrollLeft(),a.scrollTop()),b.hoverThumbs&&a.on(I.hover,e,p))):a.css("overflow","hidden"),a.data(G,e))}function D(a){y(a)()}function E(a){return this.removeOverscroll().each(function(){C(this,a)})}function F(){return this.each(function(){D(this)})}var G="overscroll";null===b.body&&b.documentElement.appendChild(b.createElement("body")),a.getComputedStyle||(a.getComputedStyle=function(a){return this.el=a,this.getPropertyValue=function(b){var c=/(\-([a-z]){1})/g;return"float"==b&&(b="styleFloat"),c.test(b)&&(b=b.replace(c,function(){return arguments[2].toUpperCase()})),a.currentStyle[b]?a.currentStyle[b]:null},this});var H={animate:function(){var b=a.requestAnimationFrame||a.webkitRequestAnimationFrame||a.mozRequestAnimationFrame||a.oRequestAnimationFrame||a.msRequestAnimationFrame||function(a){e(a,1e3/60)};return function(c){b.call(a,c)}}(),overflowScrolling:function(){var c="",d=b.createElement("div"),e=["webkit","moz","o","ms"];b.body.appendChild(d),h.each(e,function(a,b){d.style[b+"OverflowScrolling"]="touch"}),d.style.overflowScrolling="touch";var f=a.getComputedStyle(d);return f.overflowScrolling?c="overflow-scrolling":h.each(e,function(a,b){return f[b+"OverflowScrolling"]&&(c="-"+b+"-overflow-scrolling"),!c}),d.parentNode.removeChild(d),c}(),cursor:function(){var c=b.createElement("div"),d=["webkit","moz"],e="https://mail.google.com/mail/images/2/",f={grab:"url("+e+"openhand.cur), move",grabbing:"url("+e+"closedhand.cur), move"};return b.body.appendChild(c),h.each(d,function(b,d){var e,g="-"+d+"-grab";c.style.cursor=g;var h=a.getComputedStyle(c);return e=h.cursor===g,e&&(f={grab:"-"+d+"-grab",grabbing:"-"+d+"-grabbing"}),!e}),c.parentNode.removeChild(c),f}()},I={drag:"mousemove touchmove",end:"mouseup mouseleave click touchend touchcancel",hover:"mouseenter mouseleave",ignored:"select dragstart drag",scroll:"scroll",start:"mousedown touchstart",wheel:"mousewheel DOMMouseScroll"},J={captureThreshold:3,driftDecay:1.1,driftSequences:22,driftTimeout:100,scrollDelta:15,thumbOpacity:.7,thumbThickness:6,thumbTimeout:400,wheelDelta:20,wheelTicks:120},K={cancelOn:"select,input,textarea",direction:"multi",dragHold:!1,hoverThumbs:!1,scrollDelta:J.scrollDelta,showThumbs:!0,persistThumbs:!1,captureWheel:!0,wheelDelta:J.wheelDelta,wheelDirection:"multi",zIndex:999,ignoreSizing:!1};E.settings=J,h.extend(g,{overscroll:E,removeOverscroll:F})}(window,document,navigator,Math,setTimeout,clearTimeout,jQuery.fn,jQuery);
\ No newline at end of file
diff --git a/static/partials/view-repo.html b/static/partials/view-repo.html
index 6358dc637..831887c46 100644
--- a/static/partials/view-repo.html
+++ b/static/partials/view-repo.html
@@ -100,7 +100,7 @@
- Images
+ Selected Image
diff --git a/templates/index.html b/templates/index.html
index 95986c2b1..226b24f68 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -29,6 +29,7 @@
+