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:
parent
52a53f5ce2
commit
b106a31b05
4 changed files with 182 additions and 52 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';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
Reference in a new issue