diff --git a/endpoints/api/manifest.py b/endpoints/api/manifest.py
index 019c3dad2..aede35bda 100644
--- a/endpoints/api/manifest.py
+++ b/endpoints/api/manifest.py
@@ -49,9 +49,9 @@ class RepositoryManifestLabels(RepositoryParamResource):
'description': 'The value for the label',
},
'media_type': {
- 'type': ['string'],
+ 'type': ['string', 'null'],
'description': 'The media type for this label',
- 'enum': ALLOWED_LABEL_MEDIA_TYPES,
+ 'enum': ALLOWED_LABEL_MEDIA_TYPES + [None],
},
},
},
diff --git a/static/css/directives/ui/tag-operations-dialog.css b/static/css/directives/ui/tag-operations-dialog.css
index 861a3d89d..9f8934369 100644
--- a/static/css/directives/ui/tag-operations-dialog.css
+++ b/static/css/directives/ui/tag-operations-dialog.css
@@ -20,4 +20,9 @@
list-style: none;
display: inline-block;
margin: 4px;
+}
+
+.tag-operations-dialog .label-section {
+ margin-bottom: 10px;
+ padding: 6px;
}
\ No newline at end of file
diff --git a/static/directives/repo-view/repo-panel-tags.html b/static/directives/repo-view/repo-panel-tags.html
index c47f9614e..7320787e0 100644
--- a/static/directives/repo-view/repo-panel-tags.html
+++ b/static/directives/repo-view/repo-panel-tags.html
@@ -232,6 +232,10 @@
Add New Tag
+
+ Edit Labels
+
Delete Tag
@@ -272,6 +276,7 @@
+ action-handler="tagActionHandler"
+ labels-changed="handleLabelsChanged(manifest_digest)">
diff --git a/static/directives/tag-operations-dialog.html b/static/directives/tag-operations-dialog.html
index 103bb23d4..5febe8702 100644
--- a/static/directives/tag-operations-dialog.html
+++ b/static/directives/tag-operations-dialog.html
@@ -18,6 +18,27 @@
+
+
+
+
+
+
Read-only labels:
+
+
+
Mutable labels:
+
+
+
diff --git a/static/js/directives/repo-view/repo-panel-tags.js b/static/js/directives/repo-view/repo-panel-tags.js
index e1315e33b..4fca00844 100644
--- a/static/js/directives/repo-view/repo-panel-tags.js
+++ b/static/js/directives/repo-view/repo-panel-tags.js
@@ -336,6 +336,11 @@ angular.module('quay').directive('repoPanelTags', function () {
$scope.tagActionHandler.askAddTag(tag.image_id);
};
+ $scope.showLabelEditor = function(tag) {
+ if (!tag.manifest_digest) { return; }
+ $scope.tagActionHandler.showLabelEditor(tag.manifest_digest);
+ };
+
$scope.orderBy = function(predicate) {
if (predicate == $scope.options.predicate) {
$scope.options.reverse = !$scope.options.reverse;
@@ -393,6 +398,10 @@ angular.module('quay').directive('repoPanelTags', function () {
return names.join(',');
};
+
+ $scope.handleLabelsChanged = function(manifest_digest) {
+ delete $scope.labelCache[manifest_digest];
+ };
}
};
return directiveDefinitionObject;
diff --git a/static/js/directives/ui/manifest-label-list.js b/static/js/directives/ui/manifest-label-list.js
index cf42cda5f..e15708ffb 100644
--- a/static/js/directives/ui/manifest-label-list.js
+++ b/static/js/directives/ui/manifest-label-list.js
@@ -46,6 +46,12 @@ angular.module('quay').directive('manifestLabelList', function () {
});
};
+ $scope.$watch('cache', function(cache) {
+ if (cache && $scope.manifestDigest && $scope.labels && !cache[$scope.manifestDigest]) {
+ loadLabels();
+ }
+ }, true);
+
$scope.$watch('repository', loadLabels);
$scope.$watch('manifestDigest', loadLabels);
}
diff --git a/static/js/directives/ui/tag-operations-dialog.js b/static/js/directives/ui/tag-operations-dialog.js
index 648ee0f10..3dce5a9ee 100644
--- a/static/js/directives/ui/tag-operations-dialog.js
+++ b/static/js/directives/ui/tag-operations-dialog.js
@@ -13,7 +13,8 @@ angular.module('quay').directive('tagOperationsDialog', function () {
'repository': '=repository',
'actionHandler': '=actionHandler',
'imageLoader': '=imageLoader',
- 'tagChanged': '&tagChanged'
+ 'tagChanged': '&tagChanged',
+ 'labelsChanged': '&labelsChanged'
},
controller: function($scope, $element, $timeout, ApiService) {
$scope.addingTag = false;
@@ -138,6 +139,86 @@ angular.module('quay').directive('tagOperationsDialog', function () {
}, errorHandler);
};
+ $scope.editLabels = function(info, callback) {
+ var actions = [];
+ var existingMutableLabels = {};
+
+ // Build the set of adds and deletes.
+ info['updated_labels'].forEach(function(label) {
+ if (label['id']) {
+ existingMutableLabels[label['id']] = true;
+ } else {
+ actions.push({
+ 'action': 'add',
+ 'label': label
+ });
+ }
+ });
+
+ info['mutable_labels'].forEach(function(label) {
+ if (!existingMutableLabels[label['id']]) {
+ actions.push({
+ 'action': 'delete',
+ 'label': label
+ })
+ }
+ });
+
+ // Execute the add and delete label actions.
+ var currentIndex = 0;
+
+ var performAction = function() {
+ if (currentIndex >= actions.length) {
+ $scope.labelsChanged({'manifest_digest': info['manifest_digest']});
+ callback(true);
+ return;
+ }
+
+ var currentAction = actions[currentIndex];
+ currentIndex++;
+
+ var errorHandler = ApiService.errorDisplay('Could not update labels', callback);
+ switch (currentAction.action) {
+ case 'add':
+ var params = {
+ 'repository': $scope.repository.namespace + '/' + $scope.repository.name,
+ 'manifestref': info['manifest_digest']
+ };
+
+ var pieces = currentAction['label']['keyValue'].split('=', 2);
+
+ var data = {
+ 'key': pieces[0],
+ 'value': pieces[1],
+ 'media_type': null // Have backend infer the media type
+ };
+
+ ApiService.addManifestLabel(data, params).then(performAction, errorHandler);
+ break;
+
+ case 'delete':
+ var params = {
+ 'repository': $scope.repository.namespace + '/' + $scope.repository.name,
+ 'manifestref': info['manifest_digest'],
+ 'labelid': currentAction['label']['id']
+ };
+
+ ApiService.deleteManifestLabel(null, params).then(performAction, errorHandler);
+ break;
+ }
+ };
+
+ performAction();
+ };
+
+ var filterLabels = function(labels, readOnly) {
+ if (!labels) { return []; }
+
+ return labels.filter(function(label) {
+ return (label['source_type'] != 'api') == readOnly;
+ });
+ };
+
$scope.actionHandler = {
'askDeleteTag': function(tag) {
$scope.deleteTagInfo = {
@@ -159,6 +240,29 @@ angular.module('quay').directive('tagOperationsDialog', function () {
$element.find('#createOrMoveTagModal').modal('show');
},
+ 'showLabelEditor': function(manifest_digest) {
+ $scope.editLabelsInfo = {
+ 'manifest_digest': manifest_digest,
+ 'loading': true
+ };
+
+ var params = {
+ 'repository': $scope.repository.namespace + '/' + $scope.repository.name,
+ 'manifestref': manifest_digest
+ };
+
+ ApiService.listManifestLabels(null, params).then(function(resp) {
+ var labels = resp['labels'];
+
+ $scope.editLabelsInfo['readonly_labels'] = filterLabels(labels, true);
+ $scope.editLabelsInfo['mutable_labels'] = filterLabels(labels, false);
+
+ $scope.editLabelsInfo['labels'] = labels;
+ $scope.editLabelsInfo['loading'] = false;
+
+ }, ApiService.errorDisplay('Could not load manifest labels'));
+ },
+
'askRestoreTag': function(tag, image_id, opt_manifest_digest) {
if (tag.image_id == image_id) {
bootbox.alert('This is the current image for the tag');