diff --git a/static/css/quay.css b/static/css/quay.css index 697e9f45f..dc7c2fade 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -707,7 +707,7 @@ p.editable:hover i { } #image-history-container .node text { - font-size: 16px; + font-size: 15px; cursor: pointer; } @@ -721,6 +721,19 @@ p.editable:hover i { stroke: steelblue; } +#image-history-container .tags { + text-align: center; +} + +#image-history-container .tags .tag { + border-radius: 10px; + margin-right: 4px; + cursor: pointer; +} + +#image-history-container .tags .tag.current { + +} /* Overrides for the markdown editor. */ diff --git a/static/js/controllers.js b/static/js/controllers.js index 00e10ffe4..4f25a52e2 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -254,10 +254,11 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) { $scope.listImages = function() { if ($scope.imageHistory) { return; } - var imageFetch = Restangular.one('repository/' + namespace + '/' + name + '/tag/' + $scope.currentTag.name + '/images'); + var imageFetch = Restangular.one('repository/' + namespace + '/' + name + '/image'); imageFetch.get().then(function(resp) { $scope.imageHistory = resp.images; - var tree = new ImageHistoryTree(resp.images, $scope.currentTag.image, $scope.getCommentFirstLine, $scope.getTimeSince); + var tree = new ImageHistoryTree(namespace, name, resp.images, $scope.currentTag, + $scope.getCommentFirstLine, $scope.getTimeSince); tree.draw('image-history-container'); }); }; diff --git a/static/js/graphing.js b/static/js/graphing.js index 466fd1fa2..977c087df 100644 --- a/static/js/graphing.js +++ b/static/js/graphing.js @@ -1,17 +1,33 @@ +var DEPTH_HEIGHT = 100; + /** - * Based off of http://mbostock.github.io/d3/talk/20111018/tree.html by Mike Bostock (@mbostock) and modifications - * from http://stackoverflow.com/questions/18108960/d3-tree-layout-custom-vertical-layout-when-children-exceed-more-than-a-certain + * Based off of http://mbostock.github.io/d3/talk/20111018/tree.html by Mike Bostock (@mbostock) */ -function ImageHistoryTree(images, current, formatComment, formatTime) { +function ImageHistoryTree(namespace, name, images, current, formatComment, formatTime) { + /** + * The namespace of the repo. + */ + this.repoNamespace_ = namespace; + + /** + * The name of the repo. + */ + this.repoName_ = name; + /** * The images to display. */ this.images_ = images; + /** + * The current tag. + */ + this.currentTag_ = current; + /** * The current image. */ - this.currentImage_ = current; + this.currentImage_ = current.image; /** * Counter for creating unique IDs. @@ -34,14 +50,17 @@ function ImageHistoryTree(images, current, formatComment, formatTime) { * Draws the tree. */ ImageHistoryTree.prototype.draw = function(container) { + // Build the root of the tree. + var maxHeight = this.buildRoot_(); + var width = document.getElementById(container).clientWidth; - var height = width * 0.625; + var height = Math.max(width * 0.625, maxHeight * (DEPTH_HEIGHT + 10)); var margin = { top: 40, right: 120, bottom: 20, left: 120 }; var m = [margin.top, margin.right, margin.bottom, margin.left]; var w = width - m[1] - m[3]; var h = height - m[0] - m[2]; - + var tree = d3.layout.tree() .size([h, w]); @@ -71,6 +90,8 @@ ImageHistoryTree.prototype.draw = function(container) { vis.call(tip); + this.fullWidth_ = width; + this.width_ = w; this.height_ = h; @@ -80,60 +101,84 @@ ImageHistoryTree.prototype.draw = function(container) { this.tree_ = tree; + // Populate the tree. this.populate_(); }; /** - * Populates the tree. + * Returns the ancestors of the given image. */ -ImageHistoryTree.prototype.populate_ = function() { +ImageHistoryTree.prototype.getAncestors = function(image) { + var ancestorsString = image.ancestors; + // Remove the starting and ending /s. + ancestorsString = ancestorsString.substr(1, ancestorsString.length - 2); + + // Split based on /. + ancestors = ancestorsString.split('/'); + return ancestors; +}; + + +/** + * Builds the root node for the tree. + */ +ImageHistoryTree.prototype.buildRoot_ = function() { // Build the formatted JSON block for the tree. It must be of the form: // { // "name": "...", // "children": [...] // } var formatted = {}; - var currentAncestors = this.currentImage_.ancestors.split('/'); + var currentAncestors = this.getAncestors(this.currentImage_); // Build a node for each image. var imageByDBID = {}; for (var i = 0; i < this.images_.length; ++i) { var image = this.images_[i]; + var isCurrent = image.id == this.currentImage_.id; + var isHighlighted = currentAncestors.indexOf(image.dbid.toString()) >= 0; var imageNode = { "name": image.id.substr(0, 12), "children": [], "image": image, - "highlighted": jQuery.inArray(currentAncestors, image.dbid.toString()), - "current": image.id == this.currentImage_.id + "highlighted": isHighlighted || isCurrent, + "current": isCurrent, + "tags": image.tags }; imageByDBID[image.dbid] = imageNode; } // 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 = image.ancestors.split('/'); - var immediateParent = ancestors[ancestors.length - 2] * 1; + var ancestors = this.getAncestors(image); + var immediateParent = ancestors[ancestors.length - 1] * 1; var parent = imageByDBID[immediateParent]; if (parent) { parent.children.push(imageNode); } else { formatted = imageNode; } + + maxAncestorCount = Math.max(maxAncestorCount, ancestors.length); } - formatted.children.push({ - "name": "bar" - }); - this.root_ = formatted; - + return maxAncestorCount + 1; +}; + + +/** + * Populates the tree. + */ +ImageHistoryTree.prototype.populate_ = function() { // Set the position of the initial node. - this.root_.x0 = this.width_ / 2; + this.root_.x0 = this.fullWidth_ / 2; this.root_.y0 = 0; // Initialize the display to show the current path of nodes. @@ -149,6 +194,9 @@ ImageHistoryTree.prototype.update_ = function(source) { var vis = this.vis_; var diagonal = this.diagonal_; var tip = this.tip_; + var currentTag = this.currentTag_; + var repoNamespace = this.repoNamespace_; + var repoName = this.repoName_; var that = this; @@ -158,21 +206,7 @@ ImageHistoryTree.prototype.update_ = function(source) { var nodes = tree.nodes(this.root_).reverse(); // Normalize for fixed-depth. - nodes.forEach(function (d) { - d.y = d.depth * 180; - if (d.parent != null) { - d.x = d.parent.x - (d.parent.children.length-1)*30/2 - + (d.parent.children.indexOf(d))*30; - } - // if the node has too many children, go in and fix their positions to two columns. - if (d.children != null && d.children.length > 4) { - d.children.forEach(function (d, i) { - d.y = (d.depth * 180 + i % 2 * 100); - d.x = d.parent.x - (d.parent.children.length-1)*30/4 - + (d.parent.children.indexOf(d))*30/2 - i % 2 * 15; - }); - } - }); + nodes.forEach(function(d) { d.y = d.depth * DEPTH_HEIGHT; }); // Update the nodes... var node = vis.selectAll("g.node") @@ -183,22 +217,62 @@ ImageHistoryTree.prototype.update_ = function(source) { .attr("class", function(d) { return "node " + (d.current ? "current " : "") + (d.highlighted ? "highlighted " : ""); }) - .attr("transform", function(d) { return "translate(" + source.x0 + "," + source.y0 + ")"; }) - .on("click", function(d) { that.toggle_(d); that.update_(d); }); + .attr("transform", function(d) { return "translate(" + source.x0 + "," + source.y0 + ")"; }); nodeEnter.append("svg:circle") .attr("r", 1e-6) - .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; }); + .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; }) + .on("click", function(d) { that.toggle_(d); that.update_(d); }); + + // Create the group that will contain the node name and its tags. + var g = nodeEnter.append("svg:g").style("fill-opacity", 1e-6); - nodeEnter.append("svg:text") + // Add the repo ID. + g.append("svg:text") .attr("x", function(d) { return d.children || d._children ? -10 : 10; }) .attr("dy", ".35em") .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; }) .text(function(d) { return d.name; }) - .style("fill-opacity", 1e-6) + .on("click", function(d) { that.toggle_(d); that.update_(d); }) .on('mouseover', tip.show) .on('mouseout', tip.hide); + nodeEnter.selectAll("tags") + .append("svg:text") + .text("bar"); + + // Create the foreign object to hold the tags (if any). + var fo = g.append("svg:foreignObject") + .attr("x", 14) + .attr("y", 10) + .attr("width", 110) + .attr("height", DEPTH_HEIGHT - 20); + + // Add the tags. + fo.append('xhtml:div') + .attr("class", "tags") + .style("display", "none") + .html(function(d) { + var html = ''; + for (var i = 0; i < d.tags.length; ++i) { + var tag = d.tags[i]; + var kind = 'default'; + if (tag == currentTag.name) { + kind = 'success'; + } + html += ''; + html += '' + tag + ''; + html += ''; + } + return html; + }); + + // Translate the foreign object so the tags are under the ID. + fo.attr("transform", function(d, i) { + bbox = this.getBBox() + return "translate(" + [-130, 0 ] + ")"; + }); + // Transition nodes to their new position. var nodeUpdate = node.transition() .duration(duration) @@ -214,7 +288,10 @@ ImageHistoryTree.prototype.update_ = function(source) { return d._children ? "lightsteelblue" : "#fff"; }); - nodeUpdate.select("text") + nodeUpdate.select(".tags") + .style("display", ""); + + nodeUpdate.select("g") .style("fill-opacity", 1); // Transition exiting nodes to the parent's new position. @@ -226,7 +303,10 @@ ImageHistoryTree.prototype.update_ = function(source) { nodeExit.select("circle") .attr("r", 1e-6); - nodeExit.select("text") + nodeExit.select(".tags") + .style("display", "none"); + + nodeExit.select("g") .style("fill-opacity", 1e-6); // Update the links...