Merge master into ibranch
This commit is contained in:
commit
f8192a1140
34 changed files with 940 additions and 358 deletions
|
@ -24,6 +24,7 @@ angular.module('quay').directive('repoPanelTags', function () {
|
|||
};
|
||||
|
||||
$scope.iterationState = {};
|
||||
$scope.tagHistory = {};
|
||||
$scope.tagActionHandler = null;
|
||||
$scope.showingHistory = false;
|
||||
|
||||
|
@ -84,6 +85,9 @@ angular.module('quay').directive('repoPanelTags', function () {
|
|||
'count': imageMap[image_id].length,
|
||||
'tags': imageMap[image_id]
|
||||
});
|
||||
|
||||
imageMap[image_id]['color'] = colors(index);
|
||||
|
||||
++index;
|
||||
}
|
||||
});
|
||||
|
@ -119,136 +123,11 @@ angular.module('quay').directive('repoPanelTags', function () {
|
|||
|
||||
// Process each of the tags.
|
||||
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 - 1, 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) {
|
||||
if (opt_tagname) {
|
||||
$scope.options.historyFilter = opt_tagname;
|
||||
} else {
|
||||
$scope.options.historyFilter = '';
|
||||
}
|
||||
|
||||
if ($scope.showingHistory == value) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.options.historyFilter = opt_tagname ? opt_tagname : '';
|
||||
$scope.showingHistory = value;
|
||||
|
||||
if ($scope.showingHistory) {
|
||||
loadTimeline();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.toggleHistory = function() {
|
||||
|
@ -350,12 +229,22 @@ angular.module('quay').directive('repoPanelTags', function () {
|
|||
return names.join(',');
|
||||
};
|
||||
|
||||
$scope.isChecked = function(tagName, checked) {
|
||||
return checked.some(function(tag) {
|
||||
if (tag.name == tagName) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
$scope.loadTagHistory = function(tag) {
|
||||
delete $scope.tagHistory[tag.name];
|
||||
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'specificTag': tag.name,
|
||||
'limit': 5
|
||||
};
|
||||
|
||||
ApiService.listRepoTags(null, params).then(function(resp) {
|
||||
$scope.tagHistory[tag.name] = resp.tags;
|
||||
}, ApiService.errorDisplay('Could not load tag history'));
|
||||
};
|
||||
|
||||
$scope.askRevertTag = function(tag, image_id) {
|
||||
$scope.tagActionHandler.askRevertTag(tag, image_id);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -159,7 +159,7 @@ angular.module('quay').directive('buildLogsView', function () {
|
|||
|
||||
// Note: order is important here.
|
||||
var setup = filter.getSetupHtml();
|
||||
var stream = filter.addInputToStream(message);
|
||||
var stream = filter.addInputToStream(message || '');
|
||||
var teardown = filter.getTeardownHtml();
|
||||
return setup + stream + teardown;
|
||||
};
|
||||
|
|
21
static/js/directives/ui/filter-box.js
Normal file
21
static/js/directives/ui/filter-box.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* An element which displays a right-aligned control bar with an <input> for filtering a collection.
|
||||
*/
|
||||
angular.module('quay').directive('filterBox', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/filter-box.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'collection': '=collection',
|
||||
'filterModel': '=filterModel',
|
||||
'filterName': '@filterName'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -98,6 +98,7 @@ angular.module('quay').directive('logsView', function () {
|
|||
return 'Remove permission for token {token} from repository {repo}';
|
||||
}
|
||||
},
|
||||
'revert_tag': 'Tag {tag} reverted to image {image} from image {original_image}',
|
||||
'delete_tag': 'Tag {tag} deleted in repository {repo} by user {username}',
|
||||
'create_tag': 'Tag {tag} created in repository {repo} on image {image} by user {username}',
|
||||
'move_tag': 'Tag {tag} moved from image {original_image} to image {image} in repository {repo} by user {username}',
|
||||
|
@ -213,6 +214,7 @@ angular.module('quay').directive('logsView', function () {
|
|||
'delete_tag': 'Delete Tag',
|
||||
'create_tag': 'Create Tag',
|
||||
'move_tag': 'Move Tag',
|
||||
'revert_tag':' Revert Tag',
|
||||
'org_create_team': 'Create team',
|
||||
'org_delete_team': 'Delete team',
|
||||
'org_add_team_member': 'Add team member',
|
||||
|
|
157
static/js/directives/ui/repo-tag-history.js
Normal file
157
static/js/directives/ui/repo-tag-history.js
Normal file
|
@ -0,0 +1,157 @@
|
|||
/**
|
||||
* 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,
|
||||
'reversion': tag.reversion,
|
||||
'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(futureEntry.reversion ? 'revert': '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;
|
||||
});
|
|
@ -118,7 +118,7 @@ angular.module('quay').directive('robotsManager', function () {
|
|||
if ($routeParams.showRobot) {
|
||||
var index = $scope.findRobotIndexByName($routeParams.showRobot);
|
||||
if (index >= 0) {
|
||||
$scope.showRobot($scope.robots[index]);
|
||||
$scope.robotFilter = $routeParams.showRobot;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -121,6 +121,25 @@ angular.module('quay').directive('tagOperationsDialog', function () {
|
|||
}, errorHandler);
|
||||
};
|
||||
|
||||
$scope.revertTag = function(tag, image_id, callback) {
|
||||
if (!$scope.repository.can_write) { return; }
|
||||
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'tag': tag.name
|
||||
};
|
||||
|
||||
var data = {
|
||||
'image': image_id
|
||||
};
|
||||
|
||||
var errorHandler = ApiService.errorDisplay('Cannot revert tag', callback);
|
||||
ApiService.revertTag(data, params).then(function() {
|
||||
callback(true);
|
||||
markChanged([], [tag]);
|
||||
}, errorHandler);
|
||||
};
|
||||
|
||||
$scope.actionHandler = {
|
||||
'askDeleteTag': function(tag) {
|
||||
$scope.deleteTagInfo = {
|
||||
|
@ -140,6 +159,20 @@ angular.module('quay').directive('tagOperationsDialog', function () {
|
|||
$scope.addingTag = false;
|
||||
$scope.addTagForm.$setPristine();
|
||||
$element.find('#createOrMoveTagModal').modal('show');
|
||||
},
|
||||
|
||||
'askRevertTag': function(tag, image_id) {
|
||||
if (tag.image_id == image_id) {
|
||||
bootbox.alert('This is the current image for the tag');
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.revertTagInfo = {
|
||||
'tag': tag,
|
||||
'image_id': image_id
|
||||
};
|
||||
|
||||
$element.find('#revertTagModal').modal('show');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -148,11 +148,11 @@
|
|||
|
||||
// Watch for changes to the repository.
|
||||
$scope.$watch('repo', function() {
|
||||
if ($scope.tree) {
|
||||
$timeout(function() {
|
||||
$timeout(function() {
|
||||
if ($scope.tree) {
|
||||
$scope.tree.notifyResized();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Watch for changes to the tag parameter.
|
||||
|
|
Reference in a new issue