- UI improvements in prep for adding undo ability
- Move the tag history into its own directive and clean up the code
This commit is contained in:
parent
20b399318e
commit
2a77bd2c92
6 changed files with 335 additions and 312 deletions
|
@ -69,130 +69,3 @@
|
||||||
color: #999;
|
color: #999;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-panel-tags-element .history-list {
|
|
||||||
margin: 10px;
|
|
||||||
border-left: 2px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo-panel-tags-element .history-entry {
|
|
||||||
position:relative;
|
|
||||||
margin-top: 20px;
|
|
||||||
padding-left: 26px;
|
|
||||||
|
|
||||||
transition: all 350ms ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo-panel-tags-element .history-entry .history-text {
|
|
||||||
transition: transform 350ms ease-in-out, opacity 350ms ease-in-out;
|
|
||||||
overflow: hidden;
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo-panel-tags-element .history-entry.filtered-mismatch {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo-panel-tags-element .history-entry.filtered-mismatch .history-text {
|
|
||||||
height: 18px;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo-panel-tags-element .history-entry.filtered-mismatch .history-icon {
|
|
||||||
opacity: 0.5;
|
|
||||||
transform: scale(0.5, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo-panel-tags-element .history-entry .history-date-break {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo-panel-tags-element .history-entry .history-date-break:before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
background: #ccc;
|
|
||||||
top: 4px;
|
|
||||||
left: -7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo-panel-tags-element .history-entry .history-icon {
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
line-height: 33px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 20px;
|
|
||||||
color: white;
|
|
||||||
background: #ccc;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
left: -17px;
|
|
||||||
top: -4px;
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
transition: all 350ms ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo-panel-tags-element .history-entry.move .history-icon:before {
|
|
||||||
content: "\f061";
|
|
||||||
font-family: FontAwesome;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo-panel-tags-element .history-entry.create .history-icon:before {
|
|
||||||
content: "\f02b";
|
|
||||||
font-family: FontAwesome;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo-panel-tags-element .history-entry.delete .history-icon:before {
|
|
||||||
content: "\f014";
|
|
||||||
font-family: FontAwesome;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo-panel-tags-element .history-entry.move .history-icon {
|
|
||||||
background-color: #1f77b4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo-panel-tags-element .history-entry.create .history-icon {
|
|
||||||
background-color: #98df8a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo-panel-tags-element .history-entry.delete .history-icon {
|
|
||||||
background-color: #ff9896;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo-panel-tags-element .history-entry .history-icon .fa-tag {
|
|
||||||
margin-right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo-panel-tags-element .history-entry .tag-span {
|
|
||||||
display: inline-block;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 2px;
|
|
||||||
background: #eee;
|
|
||||||
padding-right: 6px;
|
|
||||||
color: black;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo-panel-tags-element .history-entry .tag-span.checked {
|
|
||||||
background: #F6FCFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo-panel-tags-element .history-entry .tag-span:before {
|
|
||||||
content: "\f02b";
|
|
||||||
font-family: FontAwesome;
|
|
||||||
margin-left: 4px;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo-panel-tags-element .history-entry .history-description {
|
|
||||||
color: #777;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo-panel-tags-element .history-entry .history-datetime {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
131
static/css/directives/ui/repo-tag-history.css
Normal file
131
static/css/directives/ui/repo-tag-history.css
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
.repo-tag-history-element .history-list {
|
||||||
|
margin: 10px;
|
||||||
|
border-left: 2px solid #eee;
|
||||||
|
margin-right: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-entry {
|
||||||
|
position:relative;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-left: 26px;
|
||||||
|
|
||||||
|
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;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-entry.filtered-mismatch {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-entry.filtered-mismatch .history-text {
|
||||||
|
height: 18px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-entry.filtered-mismatch .history-icon {
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: scale(0.5, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-entry.filtered-mismatch.current .history-icon {
|
||||||
|
background-color: #ccc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-entry .history-date-break {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-entry .history-date-break:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: #ccc;
|
||||||
|
top: 4px;
|
||||||
|
left: -7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-entry .history-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: -17px;
|
||||||
|
top: -4px;
|
||||||
|
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 20px;
|
||||||
|
color: white;
|
||||||
|
background: #ccc;
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
transition: all 350ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-entry.move .history-icon:before {
|
||||||
|
content: "\f061";
|
||||||
|
font-family: FontAwesome;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-entry.create .history-icon:before {
|
||||||
|
content: "\f02b";
|
||||||
|
font-family: FontAwesome;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-entry.delete .history-icon:before {
|
||||||
|
content: "\f014";
|
||||||
|
font-family: FontAwesome;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-entry.current.move .history-icon {
|
||||||
|
background-color: #77BFF0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-entry.current.create .history-icon {
|
||||||
|
background-color: #98df8a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-entry.current.delete .history-icon {
|
||||||
|
background-color: #ff9896;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-entry .history-icon .fa-tag {
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-entry .tag-span {
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px;
|
||||||
|
background: #eee;
|
||||||
|
padding-right: 6px;
|
||||||
|
color: black;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 .history-description {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-entry .history-datetime {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
45
static/directives/repo-tag-history.html
Normal file
45
static/directives/repo-tag-history.html
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
<div class="repo-tag-history-element">
|
||||||
|
<div class="cor-loader" ng-if="!historyEntries"></div>
|
||||||
|
|
||||||
|
<span class="co-filter-box" style="float:right">
|
||||||
|
<input class="form-control" type="text" ng-model="filter" placeholder="Filter History...">
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="history-list">
|
||||||
|
<div class="empty" ng-if="!historyEntries.length">
|
||||||
|
<div class="empty-primary-msg">This repository is empty.</div>
|
||||||
|
<div class="empty-secondary-msg">Push a tag or initiate a build to populate 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"></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 image <span class="image-link" repository="repository" image-id="entry.docker_image_id"></span>
|
||||||
|
</span>
|
||||||
|
<span ng-switch-when="delete">
|
||||||
|
was deleted
|
||||||
|
</span>
|
||||||
|
<span ng-switch-when="move">
|
||||||
|
was moved to image
|
||||||
|
<span class="image-link" repository="repository" image-id="entry.docker_image_id"></span>
|
||||||
|
from image
|
||||||
|
<span class="image-link" repository="repository" image-id="entry.old_docker_image_id"></span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="history-datetime">{{ entry.time | amDateFormat:'dddd, MMMM Do YYYY, h:mm:ss a' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="repo-panel-tags-element">
|
<div class="repo-panel-tags-element">
|
||||||
<div class="tab-header-controls">
|
<div class="tab-header-controls" ng-show="images">
|
||||||
<div class="btn-group btn-group-sm" ng-show="repository.can_write">
|
<div class="btn-group btn-group-sm" ng-show="repository.can_write">
|
||||||
<button class="btn" ng-class="!showingHistory ? 'btn-primary active' : 'btn-default'" ng-click="showHistory(false)">
|
<button class="btn" ng-class="!showingHistory ? 'btn-primary active' : 'btn-default'" ng-click="showHistory(false)">
|
||||||
<i class="fa fa-tags"></i>Current Tags
|
<i class="fa fa-tags"></i>Current Tags
|
||||||
|
@ -13,56 +13,8 @@
|
||||||
<h3 class="tab-header">Repository Tags</h3>
|
<h3 class="tab-header">Repository Tags</h3>
|
||||||
|
|
||||||
<!-- History view -->
|
<!-- History view -->
|
||||||
<div ng-show="showingHistory">
|
<div class="repo-tag-history" repository="repository" filter="options.historyFilter"
|
||||||
<div ng-show="showingHistory">
|
is-enabled="showingHistory" ng-show="showingHistory"></div>
|
||||||
<div class="cor-loader" ng-if="!tagHistoryData"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="height: 40px;">
|
|
||||||
<span class="co-filter-box" style="float:right">
|
|
||||||
<input class="form-control" type="text" ng-model="options.historyFilter" placeholder="Filter History...">
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="history-list">
|
|
||||||
<div class="empty" ng-if="!tagHistoryData.length">
|
|
||||||
<div class="empty-primary-msg">This repository is empty.</div>
|
|
||||||
<div class="empty-secondary-msg">Push a tag or initiate a build to populate this repository.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="history-entry" ng-repeat="entry in tagHistoryData"
|
|
||||||
ng-class="getEntryClasses(entry, options.historyFilter)">
|
|
||||||
<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"></div>
|
|
||||||
<div class="history-text">
|
|
||||||
<div class="history-description">
|
|
||||||
<span class="tag-span"
|
|
||||||
ng-class="isChecked(entry.tag_name, checkedTags.checked) ? 'checked' : ''"
|
|
||||||
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 image <span class="image-link" repository="repository" image-id="entry.docker_image_id"></span>
|
|
||||||
</span>
|
|
||||||
<span ng-switch-when="delete">
|
|
||||||
was deleted
|
|
||||||
</span>
|
|
||||||
<span ng-switch-when="move">
|
|
||||||
was moved to image
|
|
||||||
<span class="image-link" repository="repository" image-id="entry.docker_image_id"></span>
|
|
||||||
from image
|
|
||||||
<span class="image-link" repository="repository" image-id="entry.old_docker_image_id"></span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="history-datetime">{{ entry.time | amDateFormat:'dddd, MMMM Do YYYY, h:mm:ss a' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Normal View -->
|
<!-- Normal View -->
|
||||||
<div class="resource-view" resource="imagesResource" error-message="'Could not load images'"
|
<div class="resource-view" resource="imagesResource" error-message="'Could not load images'"
|
||||||
|
|
|
@ -119,136 +119,11 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
|
|
||||||
// Process each of the tags.
|
// Process each of the tags.
|
||||||
setTagState();
|
setTagState();
|
||||||
|
|
||||||
if ($scope.showingHistory) {
|
|
||||||
loadTimeline();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var loadTimeline = function() {
|
|
||||||
var params = {
|
|
||||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name
|
|
||||||
};
|
|
||||||
|
|
||||||
ApiService.listRepoTags(null, params).then(function(resp) {
|
|
||||||
var tagData = [];
|
|
||||||
var currentTags = {};
|
|
||||||
|
|
||||||
resp.tags.forEach(function(tag) {
|
|
||||||
var tagName = tag.name;
|
|
||||||
var imageId = tag.docker_image_id;
|
|
||||||
var oldImageId = null;
|
|
||||||
|
|
||||||
if (tag.end_ts) {
|
|
||||||
var action = 'delete';
|
|
||||||
|
|
||||||
// If the end time matches the existing start time for this tag, then this is a move
|
|
||||||
// instead of a delete.
|
|
||||||
var currentTime = tag.end_ts * 1000;
|
|
||||||
if (currentTags[tagName] && currentTags[tagName].start_ts == tag.end_ts) {
|
|
||||||
action = 'move';
|
|
||||||
|
|
||||||
// Remove the create.
|
|
||||||
var index = tagData.indexOf(currentTags[tagName]);
|
|
||||||
var createEntry = tagData.splice(index, 1)[0];
|
|
||||||
|
|
||||||
imageId = createEntry.docker_image_id;
|
|
||||||
oldImageId = tag.docker_image_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the delete/move.
|
|
||||||
tagData.push({
|
|
||||||
'tag_name': tagName,
|
|
||||||
'action': action,
|
|
||||||
'start_ts': tag.start_ts,
|
|
||||||
'end_ts': tag.end_ts,
|
|
||||||
'time': currentTime,
|
|
||||||
'docker_image_id': imageId,
|
|
||||||
'old_docker_image_id': oldImageId || ''
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tag.start_ts) {
|
|
||||||
var currentTime = tag.start_ts * 1000;
|
|
||||||
var create = {
|
|
||||||
'tag_name': tagName,
|
|
||||||
'action': 'create',
|
|
||||||
'start_ts': tag.start_ts,
|
|
||||||
'end_ts': tag.end_ts,
|
|
||||||
'time': currentTime,
|
|
||||||
'docker_image_id': tag.docker_image_id,
|
|
||||||
'old_docker_image_id': ''
|
|
||||||
};
|
|
||||||
|
|
||||||
tagData.push(create);
|
|
||||||
currentTags[tagName] = create;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tagData.sort(function(a, b) {
|
|
||||||
return b.time - a.time;
|
|
||||||
});
|
|
||||||
|
|
||||||
for (var i = tagData.length - 1; i >= 1; --i) {
|
|
||||||
var current = tagData[i];
|
|
||||||
var next = tagData[i - 1];
|
|
||||||
|
|
||||||
if (new Date(current.time).getDate() != new Date(next.time).getDate()) {
|
|
||||||
tagData.splice(i, 0, {
|
|
||||||
'date_break': true,
|
|
||||||
'date': new Date(current.time)
|
|
||||||
});
|
|
||||||
i--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tagData.length > 0) {
|
|
||||||
tagData.splice(0, 0, {
|
|
||||||
'date_break': true,
|
|
||||||
'date': new Date(tagData[0].time)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.tagHistoryData = tagData;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.getEntryClasses = function(entry, historyFilter) {
|
|
||||||
var classes = entry.action + ' ';
|
|
||||||
if (!historyFilter || !entry.action) {
|
|
||||||
return classes;
|
|
||||||
}
|
|
||||||
|
|
||||||
var parts = (historyFilter || '').split(',');
|
|
||||||
var isMatch = parts.some(function(part) {
|
|
||||||
if (part && entry.tag_name) {
|
|
||||||
isMatch = entry.tag_name.indexOf(part) >= 0;
|
|
||||||
isMatch = isMatch || entry.docker_image_id.indexOf(part) >= 0;
|
|
||||||
isMatch = isMatch || entry.old_docker_image_id.indexOf(part) >= 0;
|
|
||||||
return isMatch;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
classes += isMatch ? 'filtered-match' : 'filtered-mismatch';
|
|
||||||
return classes;
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.showHistory = function(value, opt_tagname) {
|
$scope.showHistory = function(value, opt_tagname) {
|
||||||
if (opt_tagname) {
|
$scope.options.historyFilter = opt_tagname ? opt_tagname : '';
|
||||||
$scope.options.historyFilter = opt_tagname;
|
|
||||||
} else {
|
|
||||||
$scope.options.historyFilter = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($scope.showingHistory == value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.showingHistory = value;
|
$scope.showingHistory = value;
|
||||||
|
|
||||||
if ($scope.showingHistory) {
|
|
||||||
loadTimeline();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.toggleHistory = function() {
|
$scope.toggleHistory = function() {
|
||||||
|
@ -349,14 +224,6 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
|
|
||||||
return names.join(',');
|
return names.join(',');
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.isChecked = function(tagName, checked) {
|
|
||||||
return checked.some(function(tag) {
|
|
||||||
if (tag.name == tagName) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return directiveDefinitionObject;
|
return directiveDefinitionObject;
|
||||||
|
|
155
static/js/directives/ui/repo-tag-history.js
Normal file
155
static/js/directives/ui/repo-tag-history.js
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
/**
|
||||||
|
* An element which displays its contents wrapped in an <a> tag, but only if the href is not null.
|
||||||
|
*/
|
||||||
|
angular.module('quay').directive('repoTagHistory', function () {
|
||||||
|
var directiveDefinitionObject = {
|
||||||
|
priority: 0,
|
||||||
|
templateUrl: '/static/directives/repo-tag-history.html',
|
||||||
|
replace: false,
|
||||||
|
transclude: true,
|
||||||
|
restrict: 'C',
|
||||||
|
scope: {
|
||||||
|
'repository': '=repository',
|
||||||
|
'filter': '=filter',
|
||||||
|
'isEnabled': '=isEnabled'
|
||||||
|
},
|
||||||
|
controller: function($scope, $element, ApiService) {
|
||||||
|
$scope.tagHistoryData = null;
|
||||||
|
$scope.tagHistoryLeaves = {};
|
||||||
|
|
||||||
|
var loadTimeline = function() {
|
||||||
|
if (!$scope.repository || !$scope.isEnabled) { return; }
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
'repository': $scope.repository.namespace + '/' + $scope.repository.name
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.listRepoTags(null, params).then(function(resp) {
|
||||||
|
processTags(resp.tags);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$watch('isEnabled', loadTimeline);
|
||||||
|
$scope.$watch('repository', loadTimeline);
|
||||||
|
|
||||||
|
var processTags = function(tags) {
|
||||||
|
var entries = [];
|
||||||
|
var tagEntries = {};
|
||||||
|
|
||||||
|
// For each tag, turn the tag into create, move, delete, restore, etc entries.
|
||||||
|
tags.forEach(function(tag) {
|
||||||
|
var tagName = tag.name;
|
||||||
|
var dockerImageId = tag.docker_image_id;
|
||||||
|
|
||||||
|
if (!tagEntries[tagName]) {
|
||||||
|
tagEntries[tagName] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var removeEntry = function(entry) {
|
||||||
|
entries.splice(entries.indexOf(entry), 1);
|
||||||
|
tagEntries[entry.tag_name].splice(tagEntries[entry.tag_name].indexOf(entry), 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
var addEntry = function(action, time, opt_docker_id, opt_old_docker_id) {
|
||||||
|
var entry = {
|
||||||
|
'tag_name': tagName,
|
||||||
|
'action': action,
|
||||||
|
'start_ts': tag.start_ts,
|
||||||
|
'end_ts': tag.end_ts,
|
||||||
|
'time': time * 1000, // JS expects ms, not s since epoch.
|
||||||
|
'docker_image_id': opt_docker_id || dockerImageId,
|
||||||
|
'old_docker_image_id': opt_old_docker_id || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
tagEntries[tagName].push(entry);
|
||||||
|
entries.push(entry);
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the tag has an end time, it was either deleted or moved.
|
||||||
|
if (tag.end_ts) {
|
||||||
|
// If a future entry exists with a start time equal to the end time for this tag,
|
||||||
|
// then the action was a move, rather than a delete and a create.
|
||||||
|
var currentEntries = tagEntries[tagName];
|
||||||
|
var futureEntry = currentEntries.length > 0 ? currentEntries[currentEntries.length - 1] : {};
|
||||||
|
if (futureEntry.start_ts == tag.end_ts) {
|
||||||
|
removeEntry(futureEntry);
|
||||||
|
addEntry('move', tag.end_ts, futureEntry.docker_image_id, dockerImageId);
|
||||||
|
} else {
|
||||||
|
addEntry('delete', tag.end_ts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the tag has a start time, it was created.
|
||||||
|
if (tag.start_ts) {
|
||||||
|
addEntry('create', tag.start_ts);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort the overall entries by datetime descending.
|
||||||
|
entries.sort(function(a, b) {
|
||||||
|
return b.time - a.time;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort the tag entries by datetime descending.
|
||||||
|
Object.keys(tagEntries).forEach(function(name) {
|
||||||
|
var te = tagEntries[name];
|
||||||
|
te.sort(function(a, b) {
|
||||||
|
return b.time - a.time;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add date dividers in.
|
||||||
|
for (var i = entries.length - 1; i >= 1; --i) {
|
||||||
|
var current = entries[i];
|
||||||
|
var next = entries[i - 1];
|
||||||
|
|
||||||
|
if (new Date(current.time).getDate() != new Date(next.time).getDate()) {
|
||||||
|
entries.splice(i, 0, {
|
||||||
|
'date_break': true,
|
||||||
|
'date': new Date(current.time)
|
||||||
|
});
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the top-level date divider.
|
||||||
|
if (entries.length > 0) {
|
||||||
|
entries.splice(0, 0, {
|
||||||
|
'date_break': true,
|
||||||
|
'date': new Date(entries[0].time)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.historyEntries = entries;
|
||||||
|
$scope.historyEntryMap = tagEntries;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.getEntryClasses = function(entry, historyFilter) {
|
||||||
|
if (!entry.action) { return ''; }
|
||||||
|
|
||||||
|
var classes = entry.action + ' ';
|
||||||
|
if ($scope.historyEntryMap[entry.tag_name][0] == entry) {
|
||||||
|
classes += ' current ';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!historyFilter || !entry.action) {
|
||||||
|
return classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = (historyFilter || '').split(',');
|
||||||
|
var isMatch = parts.some(function(part) {
|
||||||
|
if (part && entry.tag_name) {
|
||||||
|
isMatch = entry.tag_name.indexOf(part) >= 0;
|
||||||
|
isMatch = isMatch || entry.docker_image_id.indexOf(part) >= 0;
|
||||||
|
isMatch = isMatch || entry.old_docker_image_id.indexOf(part) >= 0;
|
||||||
|
return isMatch;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
classes += isMatch ? 'filtered-match' : 'filtered-mismatch';
|
||||||
|
return classes;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return directiveDefinitionObject;
|
||||||
|
});
|
Reference in a new issue