Add tree view for image history
This commit is contained in:
parent
b924fa5336
commit
bb5fea6a5f
3 changed files with 138 additions and 44 deletions
|
@ -707,7 +707,7 @@ p.editable:hover i {
|
||||||
}
|
}
|
||||||
|
|
||||||
#image-history-container .node text {
|
#image-history-container .node text {
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -721,6 +721,19 @@ p.editable:hover i {
|
||||||
stroke: steelblue;
|
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. */
|
/* Overrides for the markdown editor. */
|
||||||
|
|
||||||
|
|
|
@ -254,10 +254,11 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) {
|
||||||
$scope.listImages = function() {
|
$scope.listImages = function() {
|
||||||
if ($scope.imageHistory) { return; }
|
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) {
|
imageFetch.get().then(function(resp) {
|
||||||
$scope.imageHistory = resp.images;
|
$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');
|
tree.draw('image-history-container');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
* Based off of http://mbostock.github.io/d3/talk/20111018/tree.html by Mike Bostock (@mbostock)
|
||||||
* from http://stackoverflow.com/questions/18108960/d3-tree-layout-custom-vertical-layout-when-children-exceed-more-than-a-certain
|
|
||||||
*/
|
*/
|
||||||
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.
|
* The images to display.
|
||||||
*/
|
*/
|
||||||
this.images_ = images;
|
this.images_ = images;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current tag.
|
||||||
|
*/
|
||||||
|
this.currentTag_ = current;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current image.
|
* The current image.
|
||||||
*/
|
*/
|
||||||
this.currentImage_ = current;
|
this.currentImage_ = current.image;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Counter for creating unique IDs.
|
* Counter for creating unique IDs.
|
||||||
|
@ -34,8 +50,11 @@ function ImageHistoryTree(images, current, formatComment, formatTime) {
|
||||||
* Draws the tree.
|
* Draws the tree.
|
||||||
*/
|
*/
|
||||||
ImageHistoryTree.prototype.draw = function(container) {
|
ImageHistoryTree.prototype.draw = function(container) {
|
||||||
|
// Build the root of the tree.
|
||||||
|
var maxHeight = this.buildRoot_();
|
||||||
|
|
||||||
var width = document.getElementById(container).clientWidth;
|
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 margin = { top: 40, right: 120, bottom: 20, left: 120 };
|
||||||
var m = [margin.top, margin.right, margin.bottom, margin.left];
|
var m = [margin.top, margin.right, margin.bottom, margin.left];
|
||||||
|
@ -71,6 +90,8 @@ ImageHistoryTree.prototype.draw = function(container) {
|
||||||
|
|
||||||
vis.call(tip);
|
vis.call(tip);
|
||||||
|
|
||||||
|
this.fullWidth_ = width;
|
||||||
|
|
||||||
this.width_ = w;
|
this.width_ = w;
|
||||||
this.height_ = h;
|
this.height_ = h;
|
||||||
|
|
||||||
|
@ -80,60 +101,84 @@ ImageHistoryTree.prototype.draw = function(container) {
|
||||||
|
|
||||||
this.tree_ = tree;
|
this.tree_ = tree;
|
||||||
|
|
||||||
|
// Populate the tree.
|
||||||
this.populate_();
|
this.populate_();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ancestors of the given image.
|
||||||
|
*/
|
||||||
|
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.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": 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 = 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.root_ = formatted;
|
||||||
|
return maxAncestorCount + 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Populates the tree.
|
* Populates the tree.
|
||||||
*/
|
*/
|
||||||
ImageHistoryTree.prototype.populate_ = function() {
|
ImageHistoryTree.prototype.populate_ = 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('/');
|
|
||||||
|
|
||||||
// Build a node for each image.
|
|
||||||
var imageByDBID = {};
|
|
||||||
for (var i = 0; i < this.images_.length; ++i) {
|
|
||||||
var image = this.images_[i];
|
|
||||||
var imageNode = {
|
|
||||||
"name": image.id.substr(0, 12),
|
|
||||||
"children": [],
|
|
||||||
"image": image,
|
|
||||||
"highlighted": jQuery.inArray(currentAncestors, image.dbid.toString()),
|
|
||||||
"current": image.id == this.currentImage_.id
|
|
||||||
};
|
|
||||||
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.
|
|
||||||
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 parent = imageByDBID[immediateParent];
|
|
||||||
if (parent) {
|
|
||||||
parent.children.push(imageNode);
|
|
||||||
} else {
|
|
||||||
formatted = imageNode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
formatted.children.push({
|
|
||||||
"name": "bar"
|
|
||||||
});
|
|
||||||
|
|
||||||
this.root_ = formatted;
|
|
||||||
|
|
||||||
// Set the position of the initial node.
|
// Set the position of the initial node.
|
||||||
this.root_.x0 = this.width_ / 2;
|
this.root_.x0 = this.fullWidth_ / 2;
|
||||||
this.root_.y0 = 0;
|
this.root_.y0 = 0;
|
||||||
|
|
||||||
// Initialize the display to show the current path of nodes.
|
// Initialize the display to show the current path of nodes.
|
||||||
|
@ -149,6 +194,9 @@ ImageHistoryTree.prototype.update_ = function(source) {
|
||||||
var vis = this.vis_;
|
var vis = this.vis_;
|
||||||
var diagonal = this.diagonal_;
|
var diagonal = this.diagonal_;
|
||||||
var tip = this.tip_;
|
var tip = this.tip_;
|
||||||
|
var currentTag = this.currentTag_;
|
||||||
|
var repoNamespace = this.repoNamespace_;
|
||||||
|
var repoName = this.repoName_;
|
||||||
|
|
||||||
var that = this;
|
var that = this;
|
||||||
|
|
||||||
|
@ -158,21 +206,7 @@ ImageHistoryTree.prototype.update_ = function(source) {
|
||||||
var nodes = tree.nodes(this.root_).reverse();
|
var nodes = tree.nodes(this.root_).reverse();
|
||||||
|
|
||||||
// Normalize for fixed-depth.
|
// Normalize for fixed-depth.
|
||||||
nodes.forEach(function (d) {
|
nodes.forEach(function(d) { d.y = d.depth * DEPTH_HEIGHT; });
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update the nodes...
|
// Update the nodes...
|
||||||
var node = vis.selectAll("g.node")
|
var node = vis.selectAll("g.node")
|
||||||
|
@ -183,22 +217,62 @@ ImageHistoryTree.prototype.update_ = function(source) {
|
||||||
.attr("class", function(d) {
|
.attr("class", function(d) {
|
||||||
return "node " + (d.current ? "current " : "") + (d.highlighted ? "highlighted " : "");
|
return "node " + (d.current ? "current " : "") + (d.highlighted ? "highlighted " : "");
|
||||||
})
|
})
|
||||||
.attr("transform", function(d) { return "translate(" + source.x0 + "," + source.y0 + ")"; })
|
.attr("transform", function(d) { return "translate(" + source.x0 + "," + source.y0 + ")"; });
|
||||||
.on("click", function(d) { that.toggle_(d); that.update_(d); });
|
|
||||||
|
|
||||||
nodeEnter.append("svg:circle")
|
nodeEnter.append("svg:circle")
|
||||||
.attr("r", 1e-6)
|
.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); });
|
||||||
|
|
||||||
nodeEnter.append("svg:text")
|
// Create the group that will contain the node name and its tags.
|
||||||
|
var g = nodeEnter.append("svg:g").style("fill-opacity", 1e-6);
|
||||||
|
|
||||||
|
// Add the repo ID.
|
||||||
|
g.append("svg:text")
|
||||||
.attr("x", function(d) { return d.children || d._children ? -10 : 10; })
|
.attr("x", function(d) { return d.children || d._children ? -10 : 10; })
|
||||||
.attr("dy", ".35em")
|
.attr("dy", ".35em")
|
||||||
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
|
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
|
||||||
.text(function(d) { return d.name; })
|
.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('mouseover', tip.show)
|
||||||
.on('mouseout', tip.hide);
|
.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 += '<a href="/#/repository/' + repoNamespace + '/' + repoName + '/tag/' + tag + '">';
|
||||||
|
html += '<span class="label label-' + kind + ' tag">' + tag + '</span>';
|
||||||
|
html += '</a>';
|
||||||
|
}
|
||||||
|
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.
|
// Transition nodes to their new position.
|
||||||
var nodeUpdate = node.transition()
|
var nodeUpdate = node.transition()
|
||||||
.duration(duration)
|
.duration(duration)
|
||||||
|
@ -214,7 +288,10 @@ ImageHistoryTree.prototype.update_ = function(source) {
|
||||||
return d._children ? "lightsteelblue" : "#fff";
|
return d._children ? "lightsteelblue" : "#fff";
|
||||||
});
|
});
|
||||||
|
|
||||||
nodeUpdate.select("text")
|
nodeUpdate.select(".tags")
|
||||||
|
.style("display", "");
|
||||||
|
|
||||||
|
nodeUpdate.select("g")
|
||||||
.style("fill-opacity", 1);
|
.style("fill-opacity", 1);
|
||||||
|
|
||||||
// Transition exiting nodes to the parent's new position.
|
// Transition exiting nodes to the parent's new position.
|
||||||
|
@ -226,7 +303,10 @@ ImageHistoryTree.prototype.update_ = function(source) {
|
||||||
nodeExit.select("circle")
|
nodeExit.select("circle")
|
||||||
.attr("r", 1e-6);
|
.attr("r", 1e-6);
|
||||||
|
|
||||||
nodeExit.select("text")
|
nodeExit.select(".tags")
|
||||||
|
.style("display", "none");
|
||||||
|
|
||||||
|
nodeExit.select("g")
|
||||||
.style("fill-opacity", 1e-6);
|
.style("fill-opacity", 1e-6);
|
||||||
|
|
||||||
// Update the links...
|
// Update the links...
|
||||||
|
|
Reference in a new issue