Improvements for the image tracks in the tags view

1) Change to show solid dots (instead of open ones) if we are collapsing into a single track due to track count being > 5
2) Add a hover tooltip on track dots to show the tags associated with that image
3) Change the tag selection menu to only show images that are visible currently
4) Refactor the tracks code to massively reduce the amount of ng-repeats thus making the loading much faster when there are many, many tags

Fixes https://jira.coreos.com/browse/QUAY-949
This commit is contained in:
Joseph Schorr 2018-05-23 16:14:27 -04:00
parent 52a53f5ce2
commit b106a31b05
4 changed files with 182 additions and 52 deletions

View file

@ -28,6 +28,22 @@
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 {
position: absolute;
top: 0px;
@ -209,3 +225,37 @@
.repo-panel-tags-element .disabled-option a {
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;
}

View 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>

View file

@ -33,8 +33,8 @@
</div>
<div class="cor-checkable-menu-item" item-filter="imageIDFilter(it.image_id, item)"
ng-repeat="it in imageTrackEntries">
<i class="fa fa-circle-o" ng-style="{'color': it.color}"></i> {{ it.image_id.substr(0, 12) }}
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) }}
</div>
</span>
@ -116,6 +116,9 @@
style="width: 140px;">
<a ng-click="orderBy('expiration_date')" data-title="When the tag expires" data-container="body" bs-tooltip>Expires</a>
</td>
<td class="hidden-xs hidden-sm" ng-if="imageTracks.length > maxTrackCount"
style="width: 20px; position: relative;">
</td>
<td class="hidden-xs hidden-sm"
ng-class="tablePredicateClass('image_id', options.predicate, options.reverse)"
style="width: 140px;">
@ -123,10 +126,6 @@
</td>
<td class="hidden-xs hidden-sm hidden-md image-track" ng-repeat="it in imageTracks"
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="hidden-xs hidden-sm" style="width: 4px"></td>
@ -246,26 +245,38 @@
</td>
<!-- 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">
<manifest-link repository="repository" image-id="tag.image_id" manifest-digest="tag.manifest_digest"></manifest-link>
</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"
ng-if="imageTracks.length <= maxTrackCount" bindonce>
<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 class="image-track-line" bo-class="trackLineClass($parent.$parent.$parent.$index, entry)"
bo-style="{'borderColor': entry.color}"></span>
</span>
ng-if="imageTracks.length <= maxTrackCount">
<span class="image-track-dot"
ng-if="::it.entryByImageId[tag.image_id]"
ng-style="::{'borderColor': 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>
<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 class="options-col">
<i class="fa fa-download" data-title="Fetch Tag" bs-tooltip
@ -315,11 +326,10 @@
</td>
<td class="hidden-xs hidden-sm hidden-md image-track" ng-repeat="it in imageTracks"
ng-if="imageTracks.length <= maxTrackCount" bindonce>
<span ng-repeat="entry in it.entries">
<span class="image-track-line"
bo-class="trackLineExpandedClass($parent.$parent.$parent.$index, entry)"
bo-style="{'borderColor': entry.color}"></span>
</span>
<span class="image-track-line"
ng-if="::getTrackEntryForIndex(it, $parent.$parent.$index)"
ng-class="::trackLineExpandedClass(it, $parent.$parent.$parent.$index)"
ng-style="::{'borderColor': getTrackEntryForIndex(it, $parent.$parent.$parent.$index).color}"></span>
</td>
<td colspan="1" class="hidden-xs hidden-sm hidden-md"></td>
</tr>

View file

@ -110,24 +110,33 @@ angular.module('quay').directive('repoPanelTags', function () {
// Calculate the image tracks.
var colors = d3.scale.category10();
if (Object.keys(imageMap).length > 10) {
colors = d3.scale.category20();
}
var imageTracks = [];
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) {
if (imageMap[image_id].length >= 2){
// Create the track entry.
var index = imageTrackEntries.length;
var imageIndexRange = imageIndexMap[image_id];
var colorIndex = imageTrackEntries.length;
var trackEntry = {
'image_id': image_id,
'color': colors(index),
'color': colors(colorIndex),
'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);
imageMap[image_id]['color'] = colors(index);
var currentImageIndex = imageIndexMap[image_id];
trackEntryForImage[image_id] = trackEntry;
imageMap[image_id]['color'] = colors(colorIndex);
// Find the track in which we can place the entry, if any.
var existingTrack = null;
@ -140,7 +149,7 @@ angular.module('quay').directive('repoPanelTags', function () {
for (var j = 0; j < currentTrack.entries.length; ++j) {
var currentTrackEntry = currentTrack.entries[j];
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;
break;
}
@ -155,17 +164,38 @@ angular.module('quay').directive('repoPanelTags', function () {
// Add the entry to the track or create a new track if necessary.
if (existingTrack) {
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 {
var entryByImageId = {};
entryByImageId[image_id] = trackEntry;
var entryByIndex = {};
for (var j = imageIndexRange.start; j <= imageIndexRange.end; j++) {
entryByIndex[j] = trackEntry;
}
imageTracks.push({
'entries': [trackEntry]
'entries': [trackEntry],
'entryByImageId': entryByImageId,
'startIndex': imageIndexRange.start,
'endIndex': imageIndexRange.end,
'entryByIndex': entryByIndex,
});
}
imageTrackEntries.push(trackEntry);
}
});
$scope.imageMap = imageMap;
$scope.imageTracks = imageTracks;
$scope.imageTrackEntries = imageTrackEntries;
$scope.trackEntryForImage = trackEntryForImage;
$scope.options.page = 0;
@ -293,48 +323,77 @@ angular.module('quay').directive('repoPanelTags', function () {
$scope.checkedTags.setChecked($scope.tags);
};
$scope.trackLineExpandedClass = function(index, track_info) {
var startIndex = $.inArray(track_info.tags[0], $scope.tags);
var endIndex = $.inArray(track_info.tags[track_info.tags.length - 1], $scope.tags);
index += $scope.options.page * $scope.tagsPerPage;
$scope.constrastingColor = function(backgroundColor) {
// From: https://stackoverflow.com/questions/11068240/what-is-the-most-efficient-way-to-parse-a-css-color-in-javascript
function parseColor(input) {
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';
}
if (index > endIndex) {
if (index > entry.index_range.end) {
return 'after';
}
if (index >= startIndex && index < endIndex) {
if (index >= entry.index_range.start && index < entry.index_range.end) {
return 'middle';
}
return '';
};
$scope.trackLineClass = function(index, track_info) {
var startIndex = $.inArray(track_info.tags[0], $scope.tags);
var endIndex = $.inArray(track_info.tags[track_info.tags.length - 1], $scope.tags);
index += $scope.options.page * $scope.tagsPerPage;
$scope.trackLineClass = function(it, index) {
var entry = $scope.getTrackEntryForIndex(it, index);
if (!entry) {
return '';
}
if (index == startIndex) {
var adjustedIndex = index + ($scope.options.page * $scope.tagsPerPage);
if (index == entry.index_range.start) {
return 'start';
}
if (index == endIndex) {
if (index == entry.index_range.end) {
return 'end';
}
if (index > startIndex && index < endIndex) {
if (index > entry.index_range.start && index < entry.index_range.end) {
return 'middle';
}
if (index < startIndex) {
if (index < entry.index_range.start) {
return 'before';
}
if (index > endIndex) {
if (index > entry.index_range.end) {
return 'after';
}
};