Begin work on an image history tree view
This commit is contained in:
parent
d7ea1944e3
commit
9f094af1b2
9 changed files with 664 additions and 17 deletions
|
@ -251,7 +251,6 @@ def get_repository_images(namespace_name, repository_name):
|
||||||
return joined.where(Repository.name == repository_name,
|
return joined.where(Repository.name == repository_name,
|
||||||
Repository.namespace == namespace_name)
|
Repository.namespace == namespace_name)
|
||||||
|
|
||||||
|
|
||||||
def list_repository_tags(namespace_name, repository_name):
|
def list_repository_tags(namespace_name, repository_name):
|
||||||
select = RepositoryTag.select(RepositoryTag, Image)
|
select = RepositoryTag.select(RepositoryTag, Image)
|
||||||
with_repo = select.join(Repository)
|
with_repo = select.join(Repository)
|
||||||
|
|
|
@ -214,6 +214,8 @@ def image_view(image):
|
||||||
'id': image.docker_image_id,
|
'id': image.docker_image_id,
|
||||||
'created': image.created,
|
'created': image.created,
|
||||||
'comment': image.comment,
|
'comment': image.comment,
|
||||||
|
'ancestors': image.ancestors,
|
||||||
|
'dbid': image.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -689,6 +689,39 @@ p.editable:hover i {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#image-history-container .node circle {
|
||||||
|
cursor: pointer;
|
||||||
|
fill: #fff;
|
||||||
|
stroke-width: 1.5px;
|
||||||
|
stroke: #ccc;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-history-container .node.highlighted circle {
|
||||||
|
stroke: steelblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-history-container .node.current circle {
|
||||||
|
fill: steelblue;
|
||||||
|
stroke-width: 2.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-history-container .node text {
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-history-container path.link {
|
||||||
|
fill: none;
|
||||||
|
stroke: #ccc;
|
||||||
|
stroke-width: 1.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-history-container path.link.highlighted {
|
||||||
|
stroke: steelblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Overrides for the markdown editor. */
|
/* Overrides for the markdown editor. */
|
||||||
|
|
||||||
.wmd-panel .btn-toolbar {
|
.wmd-panel .btn-toolbar {
|
||||||
|
@ -784,3 +817,53 @@ p.editable:hover i {
|
||||||
.modal-backdrop.in {
|
.modal-backdrop.in {
|
||||||
opacity: 0.5 !important;
|
opacity: 0.5 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** D3 tooltip styling */
|
||||||
|
|
||||||
|
.d3-tip {
|
||||||
|
line-height: 1;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 2px;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Creates a small triangle extender for the tooltip */
|
||||||
|
.d3-tip:after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline;
|
||||||
|
font-size: 10px;
|
||||||
|
width: 100%;
|
||||||
|
line-height: 1;
|
||||||
|
color: rgba(0, 0, 0, 0.8);
|
||||||
|
content: "\25BC";
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style northward tooltips differently */
|
||||||
|
.d3-tip.n:after {
|
||||||
|
margin: -3px 0 0 0;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d3-tip .full-id {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #ddd;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d3-tip .created {
|
||||||
|
font-size: 12px;
|
||||||
|
color: white;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d3-tip .comment {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
|
@ -242,6 +242,10 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) {
|
||||||
return getMarkedDown(getFirstTextLine(commentString));
|
return getMarkedDown(getFirstTextLine(commentString));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.getTimeSince= function(createdTime) {
|
||||||
|
return moment($scope.parseDate(createdTime)).fromNow();
|
||||||
|
};
|
||||||
|
|
||||||
$scope.getMarkedDown = function(string) {
|
$scope.getMarkedDown = function(string) {
|
||||||
if (!string) { return ''; }
|
if (!string) { return ''; }
|
||||||
return getMarkedDown(string);
|
return getMarkedDown(string);
|
||||||
|
@ -253,6 +257,8 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) {
|
||||||
var imageFetch = Restangular.one('repository/' + namespace + '/' + name + '/tag/' + $scope.currentTag.name + '/images');
|
var imageFetch = Restangular.one('repository/' + namespace + '/' + name + '/tag/' + $scope.currentTag.name + '/images');
|
||||||
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);
|
||||||
|
tree.draw('image-history-container');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
283
static/js/graphing.js
Normal file
283
static/js/graphing.js
Normal file
|
@ -0,0 +1,283 @@
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
function ImageHistoryTree(images, current, formatComment, formatTime) {
|
||||||
|
/**
|
||||||
|
* The images to display.
|
||||||
|
*/
|
||||||
|
this.images_ = images;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current image.
|
||||||
|
*/
|
||||||
|
this.currentImage_ = current;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counter for creating unique IDs.
|
||||||
|
*/
|
||||||
|
this.idCounter_ = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to invoke to format a comment for an image.
|
||||||
|
*/
|
||||||
|
this.formatComment_ = formatComment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to invoke to format the time for an image.
|
||||||
|
*/
|
||||||
|
this.formatTime_ = formatTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the tree.
|
||||||
|
*/
|
||||||
|
ImageHistoryTree.prototype.draw = function(container) {
|
||||||
|
var width = document.getElementById(container).clientWidth;
|
||||||
|
var height = width * 0.625;
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
var diagonal = d3.svg.diagonal()
|
||||||
|
.projection(function(d) { return [d.x, d.y]; });
|
||||||
|
|
||||||
|
var vis = d3.select("#" + container).append("svg:svg")
|
||||||
|
.attr("width", w + m[1] + m[3])
|
||||||
|
.attr("height", h + m[0] + m[2])
|
||||||
|
.append("svg:g")
|
||||||
|
.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
|
||||||
|
|
||||||
|
var formatComment = this.formatComment_;
|
||||||
|
var formatTime = this.formatTime_;
|
||||||
|
var tip = d3.tip()
|
||||||
|
.attr('class', 'd3-tip')
|
||||||
|
.offset([-10, 0])
|
||||||
|
.html(function(d) {
|
||||||
|
var html = '';
|
||||||
|
if (d.image.comment) {
|
||||||
|
html += '<span class="comment">' + formatComment(d.image.comment) + '</span>';
|
||||||
|
}
|
||||||
|
html += '<span class="created">' + formatTime(d.image.created) + '</span>';
|
||||||
|
html += '<span class="full-id">' + d.image.id + '</span>';
|
||||||
|
return html;
|
||||||
|
})
|
||||||
|
|
||||||
|
vis.call(tip);
|
||||||
|
|
||||||
|
this.width_ = w;
|
||||||
|
this.height_ = h;
|
||||||
|
|
||||||
|
this.diagonal_ = diagonal;
|
||||||
|
this.vis_ = vis;
|
||||||
|
this.tip_ = tip;
|
||||||
|
|
||||||
|
this.tree_ = tree;
|
||||||
|
|
||||||
|
this.populate_();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populates the tree.
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
this.root_.x0 = this.width_ / 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.
|
||||||
|
*/
|
||||||
|
ImageHistoryTree.prototype.update_ = function(source) {
|
||||||
|
var tree = this.tree_;
|
||||||
|
var vis = this.vis_;
|
||||||
|
var diagonal = this.diagonal_;
|
||||||
|
var tip = this.tip_;
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
var duration = 500;
|
||||||
|
|
||||||
|
// Compute the new tree layout.
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the nodes...
|
||||||
|
var node = vis.selectAll("g.node")
|
||||||
|
.data(nodes, function(d) { return d.id || (d.id = that.idCounter_++); });
|
||||||
|
|
||||||
|
// Enter any new nodes at the parent's previous position.
|
||||||
|
var nodeEnter = node.enter().append("svg:g")
|
||||||
|
.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); });
|
||||||
|
|
||||||
|
nodeEnter.append("svg:circle")
|
||||||
|
.attr("r", 1e-6)
|
||||||
|
.style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });
|
||||||
|
|
||||||
|
nodeEnter.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('mouseover', tip.show)
|
||||||
|
.on('mouseout', tip.hide);
|
||||||
|
|
||||||
|
// Transition nodes to their new position.
|
||||||
|
var nodeUpdate = node.transition()
|
||||||
|
.duration(duration)
|
||||||
|
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
|
||||||
|
|
||||||
|
nodeUpdate.select("circle")
|
||||||
|
.attr("r", 4.5)
|
||||||
|
.attr("class", function(d) { return d._children ? "closed" : "open"; })
|
||||||
|
.style("fill", function(d) {
|
||||||
|
if (d.current) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return d._children ? "lightsteelblue" : "#fff";
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeUpdate.select("text")
|
||||||
|
.style("fill-opacity", 1);
|
||||||
|
|
||||||
|
// Transition exiting nodes to the parent's new position.
|
||||||
|
var nodeExit = node.exit().transition()
|
||||||
|
.duration(duration)
|
||||||
|
.attr("transform", function(d) { return "translate(" + source.x + "," + source.y + ")"; })
|
||||||
|
.remove();
|
||||||
|
|
||||||
|
nodeExit.select("circle")
|
||||||
|
.attr("r", 1e-6);
|
||||||
|
|
||||||
|
nodeExit.select("text")
|
||||||
|
.style("fill-opacity", 1e-6);
|
||||||
|
|
||||||
|
// Update the links...
|
||||||
|
var link = vis.selectAll("path.link")
|
||||||
|
.data(tree.links(nodes), function(d) { return d.target.id; });
|
||||||
|
|
||||||
|
// Enter any new links at the parent's previous position.
|
||||||
|
link.enter().insert("svg:path", "g")
|
||||||
|
.attr("class", function(d) {
|
||||||
|
var isHighlighted = d.target.highlighted;
|
||||||
|
return "link " + (isHighlighted ? "highlighted": "");
|
||||||
|
})
|
||||||
|
.attr("d", function(d) {
|
||||||
|
var o = {x: source.x0, y: source.y0};
|
||||||
|
return diagonal({source: o, target: o});
|
||||||
|
})
|
||||||
|
.transition()
|
||||||
|
.duration(duration)
|
||||||
|
.attr("d", diagonal);
|
||||||
|
|
||||||
|
// Transition links to their new position.
|
||||||
|
link.transition()
|
||||||
|
.duration(duration)
|
||||||
|
.attr("d", diagonal);
|
||||||
|
|
||||||
|
// Transition exiting nodes to the parent's new position.
|
||||||
|
link.exit().transition()
|
||||||
|
.duration(duration)
|
||||||
|
.attr("d", function(d) {
|
||||||
|
var o = {x: source.x, y: source.y};
|
||||||
|
return diagonal({source: o, target: o});
|
||||||
|
})
|
||||||
|
.remove();
|
||||||
|
|
||||||
|
// Stash the old positions for transition.
|
||||||
|
nodes.forEach(function(d) {
|
||||||
|
d.x0 = d.x;
|
||||||
|
d.y0 = d.y;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles children of a node.
|
||||||
|
*/
|
||||||
|
ImageHistoryTree.prototype.toggle_ = function(d) {
|
||||||
|
if (d.children) {
|
||||||
|
d._children = d.children;
|
||||||
|
d.children = null;
|
||||||
|
} else {
|
||||||
|
d.children = d._children;
|
||||||
|
d._children = null;
|
||||||
|
}
|
||||||
|
};
|
280
static/lib/d3-tip.js
vendored
Normal file
280
static/lib/d3-tip.js
vendored
Normal file
|
@ -0,0 +1,280 @@
|
||||||
|
// d3.tip
|
||||||
|
// Copyright (c) 2013 Justin Palmer
|
||||||
|
//
|
||||||
|
// Tooltips for d3.js SVG visualizations
|
||||||
|
|
||||||
|
// Public - contructs a new tooltip
|
||||||
|
//
|
||||||
|
// Returns a tip
|
||||||
|
d3.tip = function() {
|
||||||
|
var direction = d3_tip_direction,
|
||||||
|
offset = d3_tip_offset,
|
||||||
|
html = d3_tip_html,
|
||||||
|
node = initNode(),
|
||||||
|
svg = null,
|
||||||
|
point = null
|
||||||
|
|
||||||
|
function tip(vis) {
|
||||||
|
svg = getSVGNode(vis)
|
||||||
|
point = svg.createSVGPoint()
|
||||||
|
document.body.appendChild(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public - show the tooltip on the screen
|
||||||
|
//
|
||||||
|
// Returns a tip
|
||||||
|
tip.show = function() {
|
||||||
|
var content = html.apply(this, arguments),
|
||||||
|
poffset = offset.apply(this, arguments),
|
||||||
|
dir = direction.apply(this, arguments),
|
||||||
|
nodel = d3.select(node), i = 0,
|
||||||
|
coords
|
||||||
|
|
||||||
|
nodel.html(content)
|
||||||
|
.style({ opacity: 1, 'pointer-events': 'all' })
|
||||||
|
|
||||||
|
while(i--) nodel.classed(directions[i], false)
|
||||||
|
coords = direction_callbacks.get(dir).apply(this)
|
||||||
|
nodel.classed(dir, true).style({
|
||||||
|
top: (coords.top + poffset[0]) + 'px',
|
||||||
|
left: (coords.left + poffset[1]) + 'px'
|
||||||
|
})
|
||||||
|
|
||||||
|
return tip
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public - hide the tooltip
|
||||||
|
//
|
||||||
|
// Returns a tip
|
||||||
|
tip.hide = function() {
|
||||||
|
nodel = d3.select(node)
|
||||||
|
nodel.style({ opacity: 0, 'pointer-events': 'none' })
|
||||||
|
return tip
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public: Proxy attr calls to the d3 tip container. Sets or gets attribute value.
|
||||||
|
//
|
||||||
|
// n - name of the attribute
|
||||||
|
// v - value of the attribute
|
||||||
|
//
|
||||||
|
// Returns tip or attribute value
|
||||||
|
tip.attr = function(n, v) {
|
||||||
|
if (arguments.length < 2 && typeof n === 'string') {
|
||||||
|
return d3.select(node).attr(n)
|
||||||
|
} else {
|
||||||
|
var args = Array.prototype.slice.call(arguments)
|
||||||
|
d3.selection.prototype.attr.apply(d3.select(node), args)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tip
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public: Proxy style calls to the d3 tip container. Sets or gets a style value.
|
||||||
|
//
|
||||||
|
// n - name of the property
|
||||||
|
// v - value of the property
|
||||||
|
//
|
||||||
|
// Returns tip or style property value
|
||||||
|
tip.style = function(n, v) {
|
||||||
|
if (arguments.length < 2 && typeof n === 'string') {
|
||||||
|
return d3.select(node).style(n)
|
||||||
|
} else {
|
||||||
|
var args = Array.prototype.slice.call(arguments)
|
||||||
|
d3.selection.prototype.style.apply(d3.select(node), args)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tip
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public: Set or get the direction of the tooltip
|
||||||
|
//
|
||||||
|
// v - One of n(north), s(south), e(east), or w(west), nw(northwest),
|
||||||
|
// sw(southwest), ne(northeast) or se(southeast)
|
||||||
|
//
|
||||||
|
// Returns tip or direction
|
||||||
|
tip.direction = function(v) {
|
||||||
|
if (!arguments.length) return direction
|
||||||
|
direction = v == null ? v : d3.functor(v)
|
||||||
|
|
||||||
|
return tip
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public: Sets or gets the offset of the tip
|
||||||
|
//
|
||||||
|
// v - Array of [x, y] offset
|
||||||
|
//
|
||||||
|
// Returns offset or
|
||||||
|
tip.offset = function(v) {
|
||||||
|
if (!arguments.length) return offset
|
||||||
|
offset = v == null ? v : d3.functor(v)
|
||||||
|
|
||||||
|
return tip
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public: sets or gets the html value of the tooltip
|
||||||
|
//
|
||||||
|
// v - String value of the tip
|
||||||
|
//
|
||||||
|
// Returns html value or tip
|
||||||
|
tip.html = function(v) {
|
||||||
|
if (!arguments.length) return html
|
||||||
|
html = v == null ? v : d3.functor(v)
|
||||||
|
|
||||||
|
return tip
|
||||||
|
}
|
||||||
|
|
||||||
|
function d3_tip_direction() { return 'n' }
|
||||||
|
function d3_tip_offset() { return [0, 0] }
|
||||||
|
function d3_tip_html() { return ' ' }
|
||||||
|
|
||||||
|
var direction_callbacks = d3.map({
|
||||||
|
n: direction_n,
|
||||||
|
s: direction_s,
|
||||||
|
e: direction_e,
|
||||||
|
w: direction_w,
|
||||||
|
nw: direction_nw,
|
||||||
|
ne: direction_ne,
|
||||||
|
sw: direction_sw,
|
||||||
|
se: direction_se
|
||||||
|
}),
|
||||||
|
|
||||||
|
directions = direction_callbacks.keys()
|
||||||
|
|
||||||
|
function direction_n() {
|
||||||
|
var bbox = getScreenBBox()
|
||||||
|
return {
|
||||||
|
top: bbox.n.y - node.offsetHeight,
|
||||||
|
left: bbox.n.x - node.offsetWidth / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function direction_s() {
|
||||||
|
var bbox = getScreenBBox()
|
||||||
|
return {
|
||||||
|
top: bbox.s.y,
|
||||||
|
left: bbox.s.x - node.offsetWidth / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function direction_e() {
|
||||||
|
var bbox = getScreenBBox()
|
||||||
|
return {
|
||||||
|
top: bbox.e.y - node.offsetHeight / 2,
|
||||||
|
left: bbox.e.x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function direction_w() {
|
||||||
|
var bbox = getScreenBBox()
|
||||||
|
return {
|
||||||
|
top: bbox.w.y - node.offsetHeight / 2,
|
||||||
|
left: bbox.w.x - node.offsetWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function direction_nw() {
|
||||||
|
var bbox = getScreenBBox()
|
||||||
|
return {
|
||||||
|
top: bbox.nw.y - node.offsetHeight,
|
||||||
|
left: bbox.nw.x - node.offsetWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function direction_ne() {
|
||||||
|
var bbox = getScreenBBox()
|
||||||
|
return {
|
||||||
|
top: bbox.ne.y - node.offsetHeight,
|
||||||
|
left: bbox.ne.x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function direction_sw() {
|
||||||
|
var bbox = getScreenBBox()
|
||||||
|
return {
|
||||||
|
top: bbox.sw.y,
|
||||||
|
left: bbox.sw.x - node.offsetWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function direction_se() {
|
||||||
|
var bbox = getScreenBBox()
|
||||||
|
return {
|
||||||
|
top: bbox.se.y,
|
||||||
|
left: bbox.e.x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initNode() {
|
||||||
|
var node = d3.select(document.createElement('div'))
|
||||||
|
node.style({
|
||||||
|
position: 'absolute',
|
||||||
|
opacity: 0,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
boxSizing: 'border-box'
|
||||||
|
})
|
||||||
|
|
||||||
|
return node.node()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSVGNode(el) {
|
||||||
|
el = el.node()
|
||||||
|
if(el.tagName.toLowerCase() == 'svg')
|
||||||
|
return el
|
||||||
|
|
||||||
|
return el.ownerSVGElement
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private - gets the screen coordinates of a shape
|
||||||
|
//
|
||||||
|
// Given a shape on the screen, will return an SVGPoint for the directions
|
||||||
|
// n(north), s(south), e(east), w(west), ne(northeast), se(southeast), nw(northwest),
|
||||||
|
// sw(southwest).
|
||||||
|
//
|
||||||
|
// +-+-+
|
||||||
|
// | |
|
||||||
|
// + +
|
||||||
|
// | |
|
||||||
|
// +-+-+
|
||||||
|
//
|
||||||
|
// Returns an Object {n, s, e, w, nw, sw, ne, se}
|
||||||
|
function getScreenBBox() {
|
||||||
|
var target = d3.event.target,
|
||||||
|
bbox = {},
|
||||||
|
matrix = target.getScreenCTM(),
|
||||||
|
tbbox = target.getBBox(),
|
||||||
|
width = tbbox.width,
|
||||||
|
height = tbbox.height,
|
||||||
|
x = tbbox.x,
|
||||||
|
y = tbbox.y,
|
||||||
|
scrollTop = document.body.scrollTop,
|
||||||
|
scrollLeft = document.body.scrollLeft
|
||||||
|
|
||||||
|
if(document.documentElement && document.documentElement.scrollTop) {
|
||||||
|
scrollTop = document.documentElement.scrollTop
|
||||||
|
scrollLeft = document.documentElement.scrollLeft
|
||||||
|
}
|
||||||
|
|
||||||
|
point.x = x + scrollLeft
|
||||||
|
point.y = y + scrollTop
|
||||||
|
bbox.nw = point.matrixTransform(matrix)
|
||||||
|
point.x += width
|
||||||
|
bbox.ne = point.matrixTransform(matrix)
|
||||||
|
point.y += height
|
||||||
|
bbox.se = point.matrixTransform(matrix)
|
||||||
|
point.x -= width
|
||||||
|
bbox.sw = point.matrixTransform(matrix)
|
||||||
|
point.y -= height / 2
|
||||||
|
bbox.w = point.matrixTransform(matrix)
|
||||||
|
point.x += width
|
||||||
|
bbox.e = point.matrixTransform(matrix)
|
||||||
|
point.x -= width / 2
|
||||||
|
point.y -= height / 2
|
||||||
|
bbox.n = point.matrixTransform(matrix)
|
||||||
|
point.y += height
|
||||||
|
bbox.s = point.matrixTransform(matrix)
|
||||||
|
|
||||||
|
return bbox
|
||||||
|
}
|
||||||
|
|
||||||
|
return tip
|
||||||
|
};
|
5
static/lib/d3.v3.min.js
vendored
Normal file
5
static/lib/d3.v3.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -96,22 +96,7 @@
|
||||||
<i class="icon-spinner icon-spin icon-3x"></i>
|
<i class="icon-spinner icon-spin icon-3x"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-show="imageHistory">
|
<div id="image-history-container">
|
||||||
<table class="images">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<td>ID</td>
|
|
||||||
<td>Created</td>
|
|
||||||
<td>Comment</td>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tr ng-repeat="image in imageHistory">
|
|
||||||
<td class="image-id" title="{{ image.id }}">{{ image.id }}</td>
|
|
||||||
<td><span am-time-ago="parseDate(image.created)"></span></td>
|
|
||||||
<td ng-bind-html-unsafe="getCommentFirstLine(image.comment)"></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -32,8 +32,12 @@
|
||||||
<script src="static/lib/ZeroClipboard.min.js"></script>
|
<script src="static/lib/ZeroClipboard.min.js"></script>
|
||||||
<script src="static/lib/typeahead.min.js"></script>
|
<script src="static/lib/typeahead.min.js"></script>
|
||||||
|
|
||||||
|
<script src="static/lib/d3.v3.min.js" charset="utf-8"></script>
|
||||||
|
<script src="static/lib/d3-tip.js" charset="utf-8"></script>
|
||||||
|
|
||||||
<script src="static/js/app.js"></script>
|
<script src="static/js/app.js"></script>
|
||||||
<script src="static/js/controllers.js"></script>
|
<script src="static/js/controllers.js"></script>
|
||||||
|
<script src="static/js/graphing.js"></script>
|
||||||
|
|
||||||
<!-- start Mixpanel --><script type="text/javascript">
|
<!-- start Mixpanel --><script type="text/javascript">
|
||||||
var isProd = document.location.hostname === 'quay.io';
|
var isProd = document.location.hostname === 'quay.io';
|
||||||
|
|
Reference in a new issue