Redo UI of the tag history timeline and add restoration

This commit is contained in:
Joseph Schorr 2017-03-03 18:41:26 -05:00
parent e90cab4d77
commit 25db46c341
8 changed files with 256 additions and 90 deletions

View file

@ -1,7 +1,7 @@
.image-link {
display: inline-block;
white-space: nowrap;
width: 130px;
width: 120px;
}
.image-link a {

View file

@ -1,7 +1,73 @@
.repo-tag-history-element .history-list {
margin: 10px;
border-left: 2px solid #eee;
margin-right: 150px;
}
.repo-tag-history-element .co-table thead {
margin-bottom: 20px;
}
.repo-tag-history-element .co-table tbody td {
border-bottom: 0px;
}
.repo-tag-history-element .co-table td.revert-col {
font-size: 12px;
}
.repo-tag-history-element .co-table td.revert-col .tag-span {
padding-left: 4px;
padding-right: 4px;
}
.repo-tag-history-element .co-table td.icon-col {
border-bottom: 0px;
width: 32px;
height: 32px;
text-align: center;
line-height: 32px;
vertical-align: middle;
padding: 0px;
position: relative;
}
.repo-tag-history-element .co-table td.history-col {
width: 520px;
}
.repo-tag-history-element .co-table td.datetime-col {
width: 250px;
}
.repo-tag-history-element .co-table td.icon-col:after {
content: " ";
display: inline-block;
position: absolute;
top: 0px;
bottom: 0px;
left: 15px;
width: 2px;
background-color: #ddd;
z-index: 1;
}
.repo-tag-history-element .co-table td.icon-col .history-icon {
position: absolute;
top: 7px;
left: 0px;
z-index: 2;
}
.repo-tag-history-element .co-table td.icon-col .datetime-icon {
position: absolute;
top: 13px;
left: 9px;
z-index: 2;
background-color: white;
}
.repo-tag-history-element .history-row {
font-size: 13px;
color: #aaa;
}
.repo-tag-history-element .history-entry {
@ -12,10 +78,12 @@
transition: all 350ms ease-in-out;
}
.repo-tag-history-element .history-entry .history-text {
transition: transform 350ms ease-in-out, opacity 350ms ease-in-out;
.repo-tag-history-element .history-entry .history-description,
.repo-tag-history-element .history-entry .history-datetime,
.repo-tag-history-element .history-entry .history-revert {
transition: height 350ms ease-in-out, opacity 350ms ease-in-out;
overflow: hidden;
height: 40px;
height: 24px;
}
.repo-tag-history-element .co-filter-box {
@ -42,7 +110,17 @@
margin-top: 10px;
}
.repo-tag-history-element .history-entry.filtered-mismatch .history-text {
.repo-tag-history-element .history-entry.filtered-mismatch .history-datetime {
height: 18px;
opacity: 0;
}
.repo-tag-history-element .history-entry.filtered-mismatch .history-revert {
height: 18px;
opacity: 0;
}
.repo-tag-history-element .history-entry.filtered-mismatch .history-description {
height: 18px;
opacity: 0;
}
@ -64,19 +142,12 @@
content: "\f073";
font-family: FontAwesome;
position: absolute;
width: 12px;
height: 12px;
top: 1px;
left: -9px;
background: white;
}
.repo-tag-history-element .history-entry .history-icon {
position: absolute;
left: -17px;
top: -4px;
border-radius: 50%;
width: 32px;
height: 32px;
@ -100,6 +171,11 @@
font-family: FontAwesome;
}
.repo-tag-history-element .history-entry.recreate .history-icon:before {
content: "\f10d";
font-family: core-icons;
}
.repo-tag-history-element .history-entry.revert .history-icon:before {
content: "\f0e2";
font-family: FontAwesome;
@ -122,6 +198,10 @@
background-color: #98df8a;
}
.repo-tag-history-element .history-entry.current.recreate .history-icon {
background-color: #ba8adf;
}
.repo-tag-history-element .history-entry.current.delete .history-icon {
background-color: #ff9896;
}
@ -135,21 +215,23 @@
border-radius: 4px;
padding: 2px;
background: #eee;
padding-left: 6px;
padding-right: 6px;
color: black;
cursor: default;
}
.repo-tag-history-element .history-entry .tag-span span {
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
}
.repo-tag-history-element .history-entry .tag-span.checked {
background: #F6FCFF;
}
.repo-tag-history-element .history-entry .tag-span:before {
content: "\f02b";
font-family: FontAwesome;
margin-left: 4px;
margin-right: 4px;
}
.repo-tag-history-element .history-entry .image-link {
margin-left: 6px;
}
@ -159,6 +241,5 @@
}
.repo-tag-history-element .history-entry .history-datetime {
font-size: 12px;
color: #ccc;
font-size: 14px;
}

View file

@ -11,54 +11,115 @@
<div class="empty-secondary-msg">There has not been any recent tag activity on this repository.</div>
</div>
<div class="history-entry" ng-repeat="entry in historyEntries"
ng-class="getEntryClasses(entry, filter)">
<div class="history-date-break" ng-if="entry.date_break">
{{ entry.date | amDateFormat:'dddd, MMMM Do YYYY' }}
</div>
<div ng-if="!entry.date_break">
<div class="history-icon-container">
<div class="history-icon" data-title="{{ isCurrent(entry) ? 'Current view of this tag' : 'The value of this tag has changed since this action' }}" data-container="body" bs-tooltip></div>
</div>
<div class="history-text">
<div class="history-description">
<span class="tag-span"
ng-click="showHistory(true, entry.tag_name)">{{ entry.tag_name }}</span>
<span ng-switch on="entry.action">
<span ng-switch-when="create">
was created pointing to
<span class="image-link" repository="repository"
image-id="entry.docker_image_id"
manifest-digest="entry.manifest_digest"></span>
<table class="co-table" ng-if="historyEntries.length">
<thead>
<td class="icon-col"></td>
<td class="history-col">Tag Change</td>
<td class="datetime-col">Date/Time</td>
<td class="revert-col"><span ng-if="repository.can_write">Restore</span></td>
</thead>
<tbody>
<tr style="height: 20px;"><td colspan="4"></td></tr>
<tr ng-repeat="entry in historyEntries" class="history-entry" ng-class="getEntryClasses(entry, filter)">
<td ng-if="entry.date_break" class="icon-col">
<i class="fa fa-calendar datetime-icon"></i>
</td>
<td class="history-row" ng-if="entry.date_break" colspan="2">
{{ entry.date | amDateFormat:'dddd, MMMM Do YYYY' }}
</td>
<td ng-if="!entry.date_break" class="icon-col">
<div class="history-icon" data-title="{{ isCurrent(entry) ? 'Current view of this tag' : 'The value of this tag has changed since this action' }}" data-container="body" bs-tooltip></div>
</td>
<td ng-if="!entry.date_break" class="history-col">
<div class="history-description">
<span class="tag-span" data-title="{{ entry.tag_name }}" bs-tooltip><span>{{ entry.tag_name }}</span></span>
<span ng-switch on="entry.action">
<span ng-switch-when="recreate">
was recreated pointing to
<span class="image-link" repository="repository"
image-id="entry.docker_image_id"
manifest-digest="entry.manifest_digest"></span>
</span>
<span ng-switch-when="create">
was created pointing to
<span class="image-link" repository="repository"
image-id="entry.docker_image_id"
manifest-digest="entry.manifest_digest"></span>
</span>
<span ng-switch-when="delete">
was deleted
</span>
<span ng-switch-when="move">
was moved to
<span class="image-link" repository="repository"
image-id="entry.docker_image_id"
manifest-digest="entry.manifest_digest"></span>
from
<span class="image-link" repository="repository"
image-id="entry.old_docker_image_id"
manifest-digest="entry.old_manifest_digest"></span>
</span>
<span ng-switch-when="revert">
was reverted to
<span class="image-link" repository="repository"
image-id="entry.docker_image_id"
manifest-digest="entry.manifest_digest"></span>
from
<span class="image-link" repository="repository"
image-id="entry.old_docker_image_id"
manifest-digest="entry.old_manifest_digest"></span>
</span>
</span>
<span ng-switch-when="delete">
was deleted
</div>
</td>
<td ng-if="!entry.date_break" class="datetime-col">
<div class="history-datetime">{{ entry.time | amDateFormat:'dddd, MMMM Do YYYY, h:mm:ss a' }}</div>
</td>
<td class="revert-col">
<div ng-if="!entry.date_break && repository.can_write" class="history-revert">
<span ng-switch on="entry.action">
<a ng-switch-when="create" ng-click="askRestoreTag(entry, true)" ng-if="!isCurrent(entry)">
Restore <span class="tag-span"><span>{{ entry.tag_name }}</span></span> to
<span class="image-link" repository="repository"
image-id="entry.docker_image_id"
manifest-digest="entry.manifest_digest"></span>
</a>
<a ng-switch-when="recreate" ng-click="askRestoreTag(entry, true)" ng-if="!isCurrent(entry)">
Restore <span class="tag-span"><span>{{ entry.tag_name }}</span></span> to
<span class="image-link" repository="repository"
image-id="entry.docker_image_id"
manifest-digest="entry.manifest_digest"></span>
</a>
<a ng-switch-when="delete" ng-click="askRestoreTag(entry, true)">
Restore <span class="tag-span"><span>{{ entry.tag_name }}</span></span> to
<span class="image-link" repository="repository"
image-id="entry.docker_image_id"
manifest-digest="entry.manifest_digest"></span>
</a>
<a ng-switch-when="move" ng-click="askRestoreTag(entry, false)" ng-if="!isCurrent(entry)">
Restore <span class="tag-span" data-title="{{ entry.tag_name }}" bs-tooltip><span>{{ entry.tag_name }}</span></span> to
<span class="image-link" repository="repository"
image-id="entry.old_docker_image_id"
manifest-digest="entry.old_manifest_digest"></span>
</a>
<a ng-switch-when="revert" ng-click="askRestoreTag(entry, false)" ng-if="!isCurrent(entry)">
Restore <span class="tag-span" data-title="{{ entry.tag_name }}" bs-tooltip><span>{{ entry.tag_name }}</span></span> to
<span class="image-link" repository="repository"
image-id="entry.old_docker_image_id"
manifest-digest="entry.old_manifest_digest"></span>
</a>
</span>
<span ng-switch-when="move">
was moved to
<span class="image-link" repository="repository"
image-id="entry.docker_image_id"
manifest-digest="entry.manifest_digest"></span>
from
<span class="image-link" repository="repository"
image-id="entry.old_docker_image_id"
manifest-digest="entry.old_manifest_digest"></span>
</span>
<span ng-switch-when="revert">
was reverted to
<span class="image-link" repository="repository"
image-id="entry.docker_image_id"
manifest-digest="entry.manifest_digest"></span>
from
<span class="image-link" repository="repository"
image-id="entry.old_docker_image_id"
manifest-digest="entry.old_manifest_digest"></span>
</span>
</span>
</div>
<div class="history-datetime">{{ entry.time | amDateFormat:'dddd, MMMM Do YYYY, h:mm:ss a' }}</div>
</div>
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="tag-operations-dialog" repository="repository"
image-loader="imageLoader"
action-handler="tagActionHandler"></div>
</div>

View file

@ -106,19 +106,21 @@
</div>
</div>
<!-- Recert Tag Confirm -->
<!-- Restore Tag Confirm -->
<div class="cor-confirm-dialog"
dialog-context="revertTagInfo"
dialog-action="revertTag(info.tag, info.image_id, callback)"
dialog-title="Revert Tag"
dialog-action-title="Revert Tag">
dialog-context="restoreTagInfo"
dialog-action="restoreTag(info.tag, info.image_id, info.manifest_digest, callback)"
dialog-title="Restore Tag"
dialog-action-title="Restore Tag">
<div class="co-alert co-alert-warning">
This will change the image to which the tag points.
</div>
Are you sure you want to revert tag
<span class="label label-default tag">{{ revertTagInfo.tag.name }}</span> to image
<span class="image-id">{{ revertTagInfo.image_id.substr(0, 12) }}?</span>
Are you sure you want to restore tag
<span class="label label-default tag">{{ restoreTagInfo.tag.name }}</span> to image
<span class="image-link" repository="repository"
image-id="restoreTagInfo.image_id"
manifest-digest="restoreTagInfo.manifest_digest"></span>?
</div>
</div>

View file

@ -125,7 +125,13 @@ angular.module('quay').directive('logsView', function () {
return 'Remove permission for token {token} from repository {namespace}/{repo}';
}
},
'revert_tag': 'Tag {tag} reverted to image {image} from image {original_image}',
'revert_tag': function(metadata) {
if (metadata.original_image) {
return 'Tag {tag} restored to image {image} from image {original_image}';
} else {
return 'Tag {tag} recreated pointing to image {image}';
}
},
'delete_tag': 'Tag {tag} deleted in repository {namespace}/{repo} by user {username}',
'create_tag': 'Tag {tag} created in repository {namespace}/{repo} on image {image} by user {username}',
'move_tag': 'Tag {tag} moved from image {original_image} to image {image} in repository {namespace}/{repo} by user {username}',
@ -266,7 +272,7 @@ angular.module('quay').directive('logsView', function () {
'delete_tag': 'Delete Tag',
'create_tag': 'Create Tag',
'move_tag': 'Move Tag',
'revert_tag':' Revert Tag',
'revert_tag':'Restore Tag',
'org_create_team': 'Create team',
'org_delete_team': 'Delete team',
'org_add_team_member': 'Add team member',

View file

@ -11,7 +11,8 @@ angular.module('quay').directive('repoTagHistory', function () {
scope: {
'repository': '=repository',
'filter': '=filter',
'isEnabled': '=isEnabled'
'isEnabled': '=isEnabled',
'imageLoader': '=imageLoader'
},
controller: function($scope, $element, ApiService) {
$scope.tagHistoryData = null;
@ -57,6 +58,7 @@ angular.module('quay').directive('repoTagHistory', function () {
var addEntry = function(action, time, opt_docker_id, opt_old_docker_id,
opt_manifest_digest, opt_old_manifest_digest) {
var entry = {
'tag': tag,
'tag_name': tagName,
'action': action,
'start_ts': tag.start_ts,
@ -66,7 +68,7 @@ angular.module('quay').directive('repoTagHistory', function () {
'docker_image_id': opt_docker_id || dockerImageId,
'old_docker_image_id': opt_old_docker_id || '',
'manifest_digest': opt_manifest_digest || manifestDigest,
'old_manifest_digest': opt_old_manifest_digest || ''
'old_manifest_digest': opt_old_manifest_digest || null
};
tagEntries[tagName].push(entry);
@ -92,7 +94,7 @@ angular.module('quay').directive('repoTagHistory', function () {
// If the tag has a start time, it was created.
if (tag.start_ts) {
addEntry('create', tag.start_ts);
addEntry(tag.reversion ? 'recreate' : 'create', tag.start_ts);
}
});
@ -139,6 +141,14 @@ angular.module('quay').directive('repoTagHistory', function () {
return $scope.historyEntryMap[entry.tag_name][0] == entry;
};
$scope.askRestoreTag = function(entity, use_current_id) {
if ($scope.repository.can_write) {
var docker_id = use_current_id ? entity.docker_image_id : entity.old_docker_image_id;
var digest = use_current_id ? entity.manifest_digest : entity.old_manifest_digest;
$scope.tagActionHandler.askRestoreTag(entity.tag, docker_id, digest);
}
};
$scope.getEntryClasses = function(entry, historyFilter) {
if (!entry.action) { return ''; }

View file

@ -115,7 +115,7 @@ angular.module('quay').directive('tagOperationsDialog', function () {
}, errorHandler);
};
$scope.revertTag = function(tag, image_id, callback) {
$scope.restoreTag = function(tag, image_id, opt_manifest_digest, callback) {
if (!$scope.repository.can_write) { return; }
var params = {
@ -127,8 +127,12 @@ angular.module('quay').directive('tagOperationsDialog', function () {
'image': image_id
};
var errorHandler = ApiService.errorDisplay('Cannot revert tag', callback);
ApiService.revertTag(data, params).then(function() {
if (opt_manifest_digest) {
data['manifest_digest'] = opt_manifest_digest;
}
var errorHandler = ApiService.errorDisplay('Cannot restore tag', callback);
ApiService.restoreTag(data, params).then(function() {
callback(true);
markChanged([], [tag]);
}, errorHandler);
@ -155,18 +159,19 @@ angular.module('quay').directive('tagOperationsDialog', function () {
$element.find('#createOrMoveTagModal').modal('show');
},
'askRevertTag': function(tag, image_id) {
'askRestoreTag': function(tag, image_id, opt_manifest_digest) {
if (tag.image_id == image_id) {
bootbox.alert('This is the current image for the tag');
return;
}
$scope.revertTagInfo = {
$scope.restoreTagInfo = {
'tag': tag,
'image_id': image_id
'image_id': image_id,
'manifest_digest': opt_manifest_digest
};
$element.find('#revertTagModal').modal('show');
$element.find('#restoreTagModal').modal('show');
}
};
}

View file

@ -95,6 +95,7 @@
<div class="repo-tag-history"
repository="viewScope.repository"
filter="viewScope.historyFilter"
image-loader="viewScope.imageLoader"
is-enabled="historyShown"></div>
</div>