Redo UI of the tag history timeline and add restoration
This commit is contained in:
parent
e90cab4d77
commit
25db46c341
8 changed files with 256 additions and 90 deletions
|
@ -1,7 +1,7 @@
|
|||
.image-link {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
width: 130px;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.image-link a {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 ''; }
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -95,6 +95,7 @@
|
|||
<div class="repo-tag-history"
|
||||
repository="viewScope.repository"
|
||||
filter="viewScope.historyFilter"
|
||||
image-loader="viewScope.imageLoader"
|
||||
is-enabled="historyShown"></div>
|
||||
</div>
|
||||
|
||||
|
|
Reference in a new issue