diff --git a/data/registry_model/test/test_interface.py b/data/registry_model/test/test_interface.py index 95866cdda..140e366f9 100644 --- a/data/registry_model/test/test_interface.py +++ b/data/registry_model/test/test_interface.py @@ -328,6 +328,22 @@ def test_repository_tag_history(namespace, name, expected_tag_count, has_expired assert registry_model.has_expired_tag(repository_ref, "latest") +def test_repository_tag_history_future_expires(registry_model): + # Set the expiration of a tag to the future. + repository_ref = registry_model.lookup_repository("devtable", "simple") + tag = registry_model.get_repo_tag(repository_ref, "latest") + registry_model.change_repository_tag_expiration(tag, datetime.utcnow() + timedelta(days=7)) + + # List the tag history and ensure the tag is returned with the correct expiration. + history, has_more = registry_model.list_repository_tag_history(repository_ref) + assert not has_more + assert history + + for tag in history: + if tag.name == "latest": + assert tag.lifetime_end_ms is not None + + @pytest.mark.parametrize( "repositories, expected_tag_count", [([], 0), ([("devtable", "simple"), ("devtable", "building")], 1),], diff --git a/static/css/directives/ui/repo-tag-history.css b/static/css/directives/ui/repo-tag-history.css index 7563dd37d..fa7821915 100644 --- a/static/css/directives/ui/repo-tag-history.css +++ b/static/css/directives/ui/repo-tag-history.css @@ -214,6 +214,11 @@ font-family: FontAwesome; } +.repo-tag-history-element .history-entry.delete.future .history-icon:before { + content: "\f017"; + font-family: FontAwesome; +} + .repo-tag-history-element .history-entry.current.revert .history-icon { background-color: #F0C577; } @@ -234,6 +239,10 @@ background-color: #ff9896; } +.repo-tag-history-element .history-entry.current.delete.future .history-icon { + background-color: #cc8655; +} + .repo-tag-history-element .history-entry .history-icon .fa-tag { margin-right: 0px; } @@ -267,4 +276,12 @@ .repo-tag-history-element .history-entry .history-datetime { font-size: 13px; +} + +.repo-tag-history-element .co-filter-box label { + margin-right: 6px; +} + +.repo-tag-history-element .co-filter-box label input { + margin-right: 4px; } \ No newline at end of file diff --git a/static/directives/repo-tag-history.html b/static/directives/repo-tag-history.html index 79d108435..aa531274e 100644 --- a/static/directives/repo-tag-history.html +++ b/static/directives/repo-tag-history.html @@ -2,6 +2,7 @@
+ @@ -49,7 +50,8 @@ manifest-digest="entry.manifest_digest"> - was deleted + will expire + was deleted was moved to @@ -83,7 +85,7 @@ -
+
Restore to diff --git a/static/js/directives/ui/repo-tag-history.js b/static/js/directives/ui/repo-tag-history.js index cfa963111..1aad395ba 100644 --- a/static/js/directives/ui/repo-tag-history.js +++ b/static/js/directives/ui/repo-tag-history.js @@ -20,6 +20,10 @@ angular.module('quay').directive('repoTagHistory', function () { $scope.tagHistoryData = null; $scope.tagHistoryLeaves = {}; + $scope.options = { + 'showFuture': false + }; + // A delete followed by a create of a tag within this threshold is considered a move. var MOVE_THRESHOLD = 2; @@ -31,12 +35,17 @@ angular.module('quay').directive('repoTagHistory', function () { }; ApiService.listRepoTags(null, params).then(function(resp) { + $scope.cachedFullTags = resp.tags; processTags(resp.tags); }); }; $scope.$watch('isEnabled', loadTimeline); $scope.$watch('repositoryTags', loadTimeline); + $scope.$watch('options.showFuture', function() { + if (!$scope.cachedFullTags) { return; } + processTags($scope.cachedFullTags); + }); var processTags = function(tags) { var entries = []; @@ -73,6 +82,10 @@ angular.module('quay').directive('repoTagHistory', function () { 'old_manifest_digest': opt_old_manifest_digest || null }; + if (!$scope.options.showFuture && time && (time * 1000) >= new Date().getTime()) { + return; + } + tagEntries[tagName].push(entry); entries.push(entry); }; @@ -150,6 +163,11 @@ angular.module('quay').directive('repoTagHistory', function () { } }; + $scope.isFuture = function(entry) { + if (!entry) { return false; } + return entry.time >= new Date().getTime(); + }; + $scope.getEntryClasses = function(entry, historyFilter) { if (!entry.action) { return ''; } @@ -158,6 +176,10 @@ angular.module('quay').directive('repoTagHistory', function () { classes += ' current '; } + if ($scope.isFuture(entry)) { + classes += ' future '; + } + if (!historyFilter || !entry.action) { return classes; }