Merge pull request #3092 from quay/joseph.schorr/QUAY-949/image-tracks
Image track improvements
This commit is contained in:
commit
86929c16d3
7 changed files with 194 additions and 60 deletions
|
@ -28,6 +28,22 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repo-panel-tags-element .image-track-filled-dot {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
left: 2px;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
|
||||||
|
background: white;
|
||||||
|
z-index: 300;
|
||||||
|
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.repo-panel-tags-element .image-track-line {
|
.repo-panel-tags-element .image-track-line {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
|
@ -208,4 +224,38 @@
|
||||||
.repo-panel-tags-element .disabled-option,
|
.repo-panel-tags-element .disabled-option,
|
||||||
.repo-panel-tags-element .disabled-option a {
|
.repo-panel-tags-element .disabled-option a {
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-panel-tags-element .image-tag-tooltip {
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-panel-tags-element .image-tag-tooltip .image-tag-tooltip-header {
|
||||||
|
padding: 4px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-panel-tags-element .image-tag-tooltip .image-tag-tooltip-tags {
|
||||||
|
list-style: none;
|
||||||
|
padding: 10px;
|
||||||
|
padding-top: 0px;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-panel-tags-element .image-tag-tooltip .image-tag-tooltip-tags .fa-tag {
|
||||||
|
margin-right: 8px;
|
||||||
|
color: #ccc;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-panel-tags-element .image-tag-tooltip .image-tag-tooltip-tags-more {
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 4px;
|
||||||
}
|
}
|
11
static/directives/repo-view/image-tag-tooltip.html
Normal file
11
static/directives/repo-view/image-tag-tooltip.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<div class="popover image-tag-tooltip" tabindex="-1">
|
||||||
|
<div class="image-tag-tooltip-header"
|
||||||
|
ng-style="::{'backgroundColor': trackEntryForImage[tag.image_id].color,
|
||||||
|
'color': constrastingColor( trackEntryForImage[tag.image_id].color)}">
|
||||||
|
Image {{ tag.image_id.substr(0, 12) }}
|
||||||
|
</div>
|
||||||
|
<ul class="image-tag-tooltip-tags">
|
||||||
|
<li ng-repeat="tag in imageMap[tag.image_id] | limitTo:5"><i class="fa fa-tag"></i>{{ tag.name }}</li>
|
||||||
|
</ul>
|
||||||
|
<div class="image-tag-tooltip-tags-more" ng-if="imageMap[tag.image_id].length > 5">and {{ imageMap[tag.image_id].length - 5 }} more tags</div>
|
||||||
|
</div>
|
|
@ -33,8 +33,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cor-checkable-menu-item" item-filter="imageIDFilter(it.image_id, item)"
|
<div class="cor-checkable-menu-item" item-filter="imageIDFilter(it.image_id, item)"
|
||||||
ng-repeat="it in imageTrackEntries">
|
ng-repeat="it in imageTrackEntries" ng-if="::it.visible">
|
||||||
<i class="fa fa-circle-o" ng-style="{'color': it.color}"></i> {{ it.image_id.substr(0, 12) }}
|
<i class="fa fa-circle-o" ng-style="::{'color': it.color}"></i> {{ ::it.image_id.substr(0, 12) }}
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
@ -116,6 +116,9 @@
|
||||||
style="width: 140px;">
|
style="width: 140px;">
|
||||||
<a ng-click="orderBy('expiration_date')" data-title="When the tag expires" data-container="body" bs-tooltip>Expires</a>
|
<a ng-click="orderBy('expiration_date')" data-title="When the tag expires" data-container="body" bs-tooltip>Expires</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="hidden-xs hidden-sm" ng-if="imageTracks.length > maxTrackCount"
|
||||||
|
style="width: 20px; position: relative;">
|
||||||
|
</td>
|
||||||
<td class="hidden-xs hidden-sm"
|
<td class="hidden-xs hidden-sm"
|
||||||
ng-class="tablePredicateClass('image_id', options.predicate, options.reverse)"
|
ng-class="tablePredicateClass('image_id', options.predicate, options.reverse)"
|
||||||
style="width: 140px;">
|
style="width: 140px;">
|
||||||
|
@ -123,10 +126,6 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="hidden-xs hidden-sm hidden-md image-track" ng-repeat="it in imageTracks"
|
<td class="hidden-xs hidden-sm hidden-md image-track" ng-repeat="it in imageTracks"
|
||||||
ng-if="imageTracks.length <= maxTrackCount"></td>
|
ng-if="imageTracks.length <= maxTrackCount"></td>
|
||||||
<td class="hidden-xs hidden-sm" ng-if="imageTracks.length > maxTrackCount"
|
|
||||||
style="width: 20px; position: relative;">
|
|
||||||
<span class="image-track-dot" style="border-color: #ccc; top: 4px;"></span>
|
|
||||||
</td>
|
|
||||||
<td class="options-col"></td>
|
<td class="options-col"></td>
|
||||||
<td class="options-col"></td>
|
<td class="options-col"></td>
|
||||||
<td class="hidden-xs hidden-sm" style="width: 4px"></td>
|
<td class="hidden-xs hidden-sm" style="width: 4px"></td>
|
||||||
|
@ -246,26 +245,38 @@
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Manifest link -->
|
<!-- Manifest link -->
|
||||||
|
<td class="hidden-xs hidden-sm hidden-md image-track"
|
||||||
|
ng-if="imageTracks.length > maxTrackCount">
|
||||||
|
<span class="image-track-filled-dot"
|
||||||
|
ng-if="::trackEntryForImage[tag.image_id]"
|
||||||
|
ng-style="::{'backgroundColor': trackEntryForImage[tag.image_id].color}"
|
||||||
|
ng-click="::selectTrack(trackEntryForImage[tag.image_id])"
|
||||||
|
data-template-url="/static/directives/repo-view/image-tag-tooltip.html"
|
||||||
|
data-placement="left"
|
||||||
|
data-trigger="hover"
|
||||||
|
data-animation="am-flip-x"
|
||||||
|
data-auto-close="1"
|
||||||
|
bs-popover></span>
|
||||||
|
</td>
|
||||||
<td class="hidden-xs hidden-sm image-id-col">
|
<td class="hidden-xs hidden-sm image-id-col">
|
||||||
<manifest-link repository="repository" image-id="tag.image_id" manifest-digest="tag.manifest_digest"></manifest-link>
|
<manifest-link repository="repository" image-id="tag.image_id" manifest-digest="tag.manifest_digest"></manifest-link>
|
||||||
</td>
|
</td>
|
||||||
<td class="hidden-xs hidden-sm hidden-md image-track"
|
|
||||||
ng-if="imageTracks.length > maxTrackCount" bindonce>
|
|
||||||
<span ng-repeat="it in imageTracks">
|
|
||||||
<span ng-repeat="entry in it.entries">
|
|
||||||
<span class="image-track-dot" bo-if="entry.image_id == tag.image_id"
|
|
||||||
bo-style="{'borderColor': entry.color}" ng-click="selectTrack(entry)"></span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="hidden-xs hidden-sm hidden-md image-track" ng-repeat="it in imageTracks"
|
<td class="hidden-xs hidden-sm hidden-md image-track" ng-repeat="it in imageTracks"
|
||||||
ng-if="imageTracks.length <= maxTrackCount" bindonce>
|
ng-if="imageTracks.length <= maxTrackCount">
|
||||||
<span ng-repeat="entry in it.entries">
|
<span class="image-track-dot"
|
||||||
<span class="image-track-dot" bo-if="entry.image_id == tag.image_id"
|
ng-if="::it.entryByImageId[tag.image_id]"
|
||||||
bo-style="{'borderColor': entry.color}" ng-click="selectTrack(entry)"></span>
|
ng-style="::{'borderColor': trackEntryForImage[tag.image_id].color}"
|
||||||
<span class="image-track-line" bo-class="trackLineClass($parent.$parent.$parent.$index, entry)"
|
ng-click="::selectTrack(trackEntryForImage[tag.image_id])"
|
||||||
bo-style="{'borderColor': entry.color}"></span>
|
data-template-url="/static/directives/repo-view/image-tag-tooltip.html"
|
||||||
</span>
|
data-placement="left"
|
||||||
|
data-trigger="hover"
|
||||||
|
data-animation="am-flip-x"
|
||||||
|
data-auto-close="1"
|
||||||
|
bs-popover></span>
|
||||||
|
<span class="image-track-line"
|
||||||
|
ng-if="::getTrackEntryForIndex(it, $parent.$parent.$index)"
|
||||||
|
ng-class="::trackLineClass(it, $parent.$parent.$parent.$index)"
|
||||||
|
ng-style="::{'borderColor': getTrackEntryForIndex(it, $parent.$parent.$parent.$index).color}"></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="options-col">
|
<td class="options-col">
|
||||||
<i class="fa fa-download" data-title="Fetch Tag" bs-tooltip
|
<i class="fa fa-download" data-title="Fetch Tag" bs-tooltip
|
||||||
|
@ -315,11 +326,10 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="hidden-xs hidden-sm hidden-md image-track" ng-repeat="it in imageTracks"
|
<td class="hidden-xs hidden-sm hidden-md image-track" ng-repeat="it in imageTracks"
|
||||||
ng-if="imageTracks.length <= maxTrackCount" bindonce>
|
ng-if="imageTracks.length <= maxTrackCount" bindonce>
|
||||||
<span ng-repeat="entry in it.entries">
|
<span class="image-track-line"
|
||||||
<span class="image-track-line"
|
ng-if="::getTrackEntryForIndex(it, $parent.$parent.$index)"
|
||||||
bo-class="trackLineExpandedClass($parent.$parent.$parent.$index, entry)"
|
ng-class="::trackLineExpandedClass(it, $parent.$parent.$parent.$index)"
|
||||||
bo-style="{'borderColor': entry.color}"></span>
|
ng-style="::{'borderColor': getTrackEntryForIndex(it, $parent.$parent.$parent.$index).color}"></span>
|
||||||
</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td colspan="1" class="hidden-xs hidden-sm hidden-md"></td>
|
<td colspan="1" class="hidden-xs hidden-sm hidden-md"></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -110,24 +110,33 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
|
|
||||||
// Calculate the image tracks.
|
// Calculate the image tracks.
|
||||||
var colors = d3.scale.category10();
|
var colors = d3.scale.category10();
|
||||||
|
if (Object.keys(imageMap).length > 10) {
|
||||||
|
colors = d3.scale.category20();
|
||||||
|
}
|
||||||
|
|
||||||
var imageTracks = [];
|
var imageTracks = [];
|
||||||
var imageTrackEntries = [];
|
var imageTrackEntries = [];
|
||||||
|
var trackEntryForImage = {};
|
||||||
|
|
||||||
|
var visibleStartIndex = ($scope.options.page * $scope.tagsPerPage);
|
||||||
|
var visibleEndIndex = (($scope.options.page + 1) * $scope.tagsPerPage);
|
||||||
|
|
||||||
imageIDs.sort().map(function(image_id) {
|
imageIDs.sort().map(function(image_id) {
|
||||||
if (imageMap[image_id].length >= 2){
|
if (imageMap[image_id].length >= 2){
|
||||||
// Create the track entry.
|
// Create the track entry.
|
||||||
var index = imageTrackEntries.length;
|
var imageIndexRange = imageIndexMap[image_id];
|
||||||
|
var colorIndex = imageTrackEntries.length;
|
||||||
var trackEntry = {
|
var trackEntry = {
|
||||||
'image_id': image_id,
|
'image_id': image_id,
|
||||||
'color': colors(index),
|
'color': colors(colorIndex),
|
||||||
'count': imageMap[image_id].length,
|
'count': imageMap[image_id].length,
|
||||||
'tags': imageMap[image_id]
|
'tags': imageMap[image_id],
|
||||||
|
'index_range': imageIndexRange,
|
||||||
|
'visible': visibleStartIndex <= imageIndexRange.end && imageIndexRange.start <= visibleEndIndex,
|
||||||
};
|
};
|
||||||
|
|
||||||
imageTrackEntries.push(trackEntry);
|
trackEntryForImage[image_id] = trackEntry;
|
||||||
|
imageMap[image_id]['color'] = colors(colorIndex);
|
||||||
imageMap[image_id]['color'] = colors(index);
|
|
||||||
var currentImageIndex = imageIndexMap[image_id];
|
|
||||||
|
|
||||||
// Find the track in which we can place the entry, if any.
|
// Find the track in which we can place the entry, if any.
|
||||||
var existingTrack = null;
|
var existingTrack = null;
|
||||||
|
@ -140,7 +149,7 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
for (var j = 0; j < currentTrack.entries.length; ++j) {
|
for (var j = 0; j < currentTrack.entries.length; ++j) {
|
||||||
var currentTrackEntry = currentTrack.entries[j];
|
var currentTrackEntry = currentTrack.entries[j];
|
||||||
var entryInfo = imageIndexMap[currentTrackEntry.image_id];
|
var entryInfo = imageIndexMap[currentTrackEntry.image_id];
|
||||||
if (Math.max(entryInfo.start, currentImageIndex.start) <= Math.min(entryInfo.end, currentImageIndex.end)) {
|
if (Math.max(entryInfo.start, imageIndexRange.start) <= Math.min(entryInfo.end, imageIndexRange.end)) {
|
||||||
canAddToCurrentTrack = false;
|
canAddToCurrentTrack = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -155,17 +164,38 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
// Add the entry to the track or create a new track if necessary.
|
// Add the entry to the track or create a new track if necessary.
|
||||||
if (existingTrack) {
|
if (existingTrack) {
|
||||||
existingTrack.entries.push(trackEntry)
|
existingTrack.entries.push(trackEntry)
|
||||||
|
existingTrack.entryByImageId[image_id] = trackEntry;
|
||||||
|
existingTrack.endIndex = Math.max(existingTrack.endIndex, imageIndexRange.end);
|
||||||
|
|
||||||
|
for (var j = imageIndexRange.start; j <= imageIndexRange.end; j++) {
|
||||||
|
existingTrack.entryByIndex[j] = trackEntry;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
var entryByImageId = {};
|
||||||
|
entryByImageId[image_id] = trackEntry;
|
||||||
|
|
||||||
|
var entryByIndex = {};
|
||||||
|
for (var j = imageIndexRange.start; j <= imageIndexRange.end; j++) {
|
||||||
|
entryByIndex[j] = trackEntry;
|
||||||
|
}
|
||||||
|
|
||||||
imageTracks.push({
|
imageTracks.push({
|
||||||
'entries': [trackEntry]
|
'entries': [trackEntry],
|
||||||
|
'entryByImageId': entryByImageId,
|
||||||
|
'startIndex': imageIndexRange.start,
|
||||||
|
'endIndex': imageIndexRange.end,
|
||||||
|
'entryByIndex': entryByIndex,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
imageTrackEntries.push(trackEntry);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.imageMap = imageMap;
|
$scope.imageMap = imageMap;
|
||||||
$scope.imageTracks = imageTracks;
|
$scope.imageTracks = imageTracks;
|
||||||
$scope.imageTrackEntries = imageTrackEntries;
|
$scope.imageTrackEntries = imageTrackEntries;
|
||||||
|
$scope.trackEntryForImage = trackEntryForImage;
|
||||||
|
|
||||||
$scope.options.page = 0;
|
$scope.options.page = 0;
|
||||||
|
|
||||||
|
@ -293,48 +323,77 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
$scope.checkedTags.setChecked($scope.tags);
|
$scope.checkedTags.setChecked($scope.tags);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.trackLineExpandedClass = function(index, track_info) {
|
$scope.constrastingColor = function(backgroundColor) {
|
||||||
var startIndex = $.inArray(track_info.tags[0], $scope.tags);
|
// From: https://stackoverflow.com/questions/11068240/what-is-the-most-efficient-way-to-parse-a-css-color-in-javascript
|
||||||
var endIndex = $.inArray(track_info.tags[track_info.tags.length - 1], $scope.tags);
|
function parseColor(input) {
|
||||||
index += $scope.options.page * $scope.tagsPerPage;
|
m = input.match(/^#([0-9a-f]{6})$/i)[1];
|
||||||
|
return [
|
||||||
|
parseInt(m.substr(0,2),16),
|
||||||
|
parseInt(m.substr(2,2),16),
|
||||||
|
parseInt(m.substr(4,2),16)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
if (index < startIndex) {
|
var rgb = parseColor(backgroundColor);
|
||||||
|
|
||||||
|
// From W3C standard.
|
||||||
|
var o = Math.round(((parseInt(rgb[0]) * 299) + (parseInt(rgb[1]) * 587) + (parseInt(rgb[2]) * 114)) / 1000);
|
||||||
|
return (o > 150) ? 'black' : 'white';
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.getTrackEntryForIndex = function(it, index) {
|
||||||
|
index += $scope.options.page * $scope.tagsPerPage;
|
||||||
|
return it.entryByIndex[index];
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.trackLineExpandedClass = function(it, index, track_info) {
|
||||||
|
var entry = $scope.getTrackEntryForIndex(it, index);
|
||||||
|
if (!entry) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
var adjustedIndex = index + ($scope.options.page * $scope.tagsPerPage);
|
||||||
|
|
||||||
|
if (index < entry.index_range.start) {
|
||||||
return 'before';
|
return 'before';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index > endIndex) {
|
if (index > entry.index_range.end) {
|
||||||
return 'after';
|
return 'after';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index >= startIndex && index < endIndex) {
|
if (index >= entry.index_range.start && index < entry.index_range.end) {
|
||||||
return 'middle';
|
return 'middle';
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.trackLineClass = function(index, track_info) {
|
$scope.trackLineClass = function(it, index) {
|
||||||
var startIndex = $.inArray(track_info.tags[0], $scope.tags);
|
var entry = $scope.getTrackEntryForIndex(it, index);
|
||||||
var endIndex = $.inArray(track_info.tags[track_info.tags.length - 1], $scope.tags);
|
if (!entry) {
|
||||||
index += $scope.options.page * $scope.tagsPerPage;
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
if (index == startIndex) {
|
var adjustedIndex = index + ($scope.options.page * $scope.tagsPerPage);
|
||||||
|
|
||||||
|
if (index == entry.index_range.start) {
|
||||||
return 'start';
|
return 'start';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index == endIndex) {
|
if (index == entry.index_range.end) {
|
||||||
return 'end';
|
return 'end';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index > startIndex && index < endIndex) {
|
if (index > entry.index_range.start && index < entry.index_range.end) {
|
||||||
return 'middle';
|
return 'middle';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index < startIndex) {
|
if (index < entry.index_range.start) {
|
||||||
return 'before';
|
return 'before';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index > endIndex) {
|
if (index > entry.index_range.end) {
|
||||||
return 'after';
|
return 'after';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
3
static/lib/angular-strap.compat.min.js
vendored
Normal file
3
static/lib/angular-strap.compat.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
11
static/lib/angular-strap.min.js
vendored
11
static/lib/angular-strap.min.js
vendored
File diff suppressed because one or more lines are too long
6
static/lib/angular-strap.tpl.min.js
vendored
6
static/lib/angular-strap.tpl.min.js
vendored
File diff suppressed because one or more lines are too long
Reference in a new issue