Add UI support for multiple operations on keys

This commit is contained in:
Joseph Schorr 2016-04-27 17:44:44 -04:00 committed by Jimmy Zelinskie
parent 726cb5fe6a
commit a55e92bc95
6 changed files with 283 additions and 22 deletions

View file

@ -1208,9 +1208,9 @@ a:focus {
.co-checkable-menu-state.some:after { .co-checkable-menu-state.some:after {
content: "-"; content: "-";
font-size: 19px; font-size: 24px;
top: -6px; top: -10px;
left: 3px; left: 4px;
} }
@media (min-width: 768px) { @media (min-width: 768px) {

View file

@ -96,3 +96,21 @@
white-space: nowrap; white-space: nowrap;
vertical-align: middle; vertical-align: middle;
} }
.service-keys-manager-element .keys-list {
list-style: circle;
padding: 10px;
padding-left: 40px;
}
.service-keys-manager-element .keys-list li {
padding: 4px;
font-family: Consolas, "Lucida Console", Monaco, monospace;
}
.service-keys-manager-element .expiration-form .datetime-picker {
margin-top: 4px;
display: block;
margin-bottom: 2px;
}

View file

@ -27,17 +27,17 @@
<div class="co-check-bar"> <div class="co-check-bar">
<span class="cor-checkable-menu" controller="checkedTags"> <span class="cor-checkable-menu" controller="checkedTags">
<div class="cor-checkable-menu-item" item-filter="allTagFilter"> <div class="cor-checkable-menu-item" item-filter="allTagFilter(item)">
<i class="fa fa-check-square-o"></i>All Tags <i class="fa fa-check-square-o"></i>All Tags
</div> </div>
<div class="cor-checkable-menu-item" item-filter="noTagFilter(tag)"> <div class="cor-checkable-menu-item" item-filter="noTagFilter(item)">
<i class="fa fa-square-o"></i>No Tags <i class="fa fa-square-o"></i>No Tags
</div> </div>
<div class="cor-checkable-menu-item" item-filter="commitTagFilter(tag)"> <div class="cor-checkable-menu-item" item-filter="commitTagFilter(item)">
<i class="fa fa-git"></i>Commit SHAs <i class="fa fa-git"></i>Commit SHAs
</div> </div>
<div class="cor-checkable-menu-item" item-filter="imageIDFilter(it.image_id, tag)" <div class="cor-checkable-menu-item" item-filter="imageIDFilter(it.image_id, item)"
ng-repeat="it in imageTracks"> ng-repeat="it in imageTracks">
<i class="fa fa-circle-o" ng-style="{'color': it.color}"></i> {{ it.image_id.substr(0, 12) }} <i class="fa fa-circle-o" ng-style="{'color': it.color}"></i> {{ it.image_id.substr(0, 12) }}
</div> </div>

View file

@ -11,12 +11,56 @@
<a href="https://tectonic.com/quay-enterprise/docs/latest/build-support.html" target="_blank">build workers</a>. <a href="https://tectonic.com/quay-enterprise/docs/latest/build-support.html" target="_blank">build workers</a>.
</div> </div>
<span class="co-filter-box" ng-if="keys.length"> <div class="co-check-bar" ng-show="keys.length">
<span class="cor-checkable-menu" controller="checkedKeys">
<div class="cor-checkable-menu-item" item-filter="allKeyFilter(item)">
<i class="fa fa-check-square-o"></i>All Keys
</div>
<div class="cor-checkable-menu-item" item-filter="noKeyFilter(item)">
<i class="fa fa-square-o"></i>No Keys
</div>
<div class="cor-checkable-menu-item" item-filter="unapprovedKeyFilter(item)">
<i class="fa fa-question-circle"></i>Unapproved Keys
</div>
<div class="cor-checkable-menu-item" item-filter="expiredKeyFilter(item)">
<i class="fa fa-warning"></i>Expired Keys
</div>
</span>
<span class="co-checked-actions" ng-if="checkedKeys.checked.length">
<button class="btn btn-primary"
ng-click="askApproveMultipleKeys(checkedKeys.checked)"
ng-show="allRequireApproval(checkedKeys.checked)">
<i class="fa fa-check"></i><span class="text">Approve Keys</span>
</button>
<button class="btn btn-primary"
ng-click="askChangeExpirationMultipleKeys(checkedKeys.checked)"
ng-if="allExpired(checkedKeys.checked)">
<i class="fa fa-refresh"></i>
<span class="text">Revive Keys</span>
</button>
<button class="btn btn-default"
ng-click="askChangeExpirationMultipleKeys(checkedKeys.checked)"
ng-if="!allExpired(checkedKeys.checked)">
<i class="fa fa-clock-o"></i>
<span class="text">Change Keys Expiration</span>
</button>
<button class="btn btn-default"
ng-click="askDeleteMultipleKeys(checkedKeys.checked)">
<i class="fa fa-times"></i><span class="text">Delete Keys</span>
</button>
</span>
<span class="co-filter-box">
<span class="filter-message" ng-if="options.filter"> <span class="filter-message" ng-if="options.filter">
Showing {{ orderedKeys.entries.length }} of {{ keys.length }} keys Showing {{ orderedKeys.entries.length }} of {{ keys.length }} keys
</span> </span>
<input class="form-control" type="text" ng-model="options.filter" placeholder="Filter Keys..."> <input class="form-control" type="text" ng-model="options.filter" placeholder="Filter Keys...">
</span> </span>
</div>
<!-- Table --> <!-- Table -->
<div class="empty" ng-if="!keys.length" style="margin-top: 20px;"> <div class="empty" ng-if="!keys.length" style="margin-top: 20px;">
@ -26,6 +70,7 @@
<table class="co-table" ng-show="keys.length"> <table class="co-table" ng-show="keys.length">
<thead> <thead>
<td class="checkbox-col"></td>
<td class="caret-col"></td> <td class="caret-col"></td>
<td ng-class="TableService.tablePredicateClass('name', options.predicate, options.reverse)"> <td ng-class="TableService.tablePredicateClass('name', options.predicate, options.reverse)">
<a href="javascript:void(0)" ng-click="TableService.orderBy('name', options)">Name</a> <a href="javascript:void(0)" ng-click="TableService.orderBy('name', options)">Name</a>
@ -44,8 +89,12 @@
</td> </td>
<td class="hidden-xs options-col"></td> <td class="hidden-xs options-col"></td>
</thead> </thead>
<tbody ng-repeat="key in orderedKeys.visibleEntries" bindonce> <tbody class="co-checkable-row"
ng-repeat="key in orderedKeys.visibleEntries"
ng-class="checkedKeys.isChecked(key, checkedKeys.checked) ? 'checked' : ''"
bindonce>
<tr> <tr>
<td><span class="cor-checkable-item" controller="checkedKeys" item="key"></span></td>
<td class="caret-col"> <td class="caret-col">
<span ng-click="toggleDetails(key)"> <span ng-click="toggleDetails(key)">
<i class="fa" <i class="fa"
@ -88,7 +137,6 @@
<i class="fa fa-refresh"></i>Approved via key rotation <i class="fa fa-refresh"></i>Approved via key rotation
</span> </span>
<span class="approval-required" bo-if="!key.approval"> <span class="approval-required" bo-if="!key.approval">
<i class="fa fa-warning"></i>
Awaiting Approval <a ng-click="showApproveKey(key)">Approve Now</a> Awaiting Approval <a ng-click="showApproveKey(key)">Approve Now</a>
</span> </span>
</td> </td>
@ -130,6 +178,24 @@
</div> </div>
</div> </div>
<!-- Change Keys Expiration Confirm -->
<div class="cor-confirm-dialog"
dialog-context="changeKeysInfo"
dialog-action="changeKeysExpiration(changeKeysInfo, callback)"
dialog-title="Change Service Keys Expiration"
dialog-action-title="Change Expiration">
<form class="expiration-form">
Please choose the new expiration date and time (if any) for the following keys:
<ul class="keys-list">
<li ng-repeat="key in changeKeysInfo.keys">{{ getKeyTitle(key) }}</li>
</ul>
<label>Expiration Date:</label>
<span class="datetime-picker" datetime="changeKeysInfo.expiration_date"></span>
<span class="co-help-text">
If specified, the date and time at which the keys expire. It is highly recommended to have an expiration date.
</span>
</form>
</div>
<!-- Change Key Expiration Confirm --> <!-- Change Key Expiration Confirm -->
<div class="cor-confirm-dialog" <div class="cor-confirm-dialog"
@ -137,7 +203,8 @@
dialog-action="changeKeyExpiration(context.expirationChangeInfo, callback)" dialog-action="changeKeyExpiration(context.expirationChangeInfo, callback)"
dialog-title="Change Service Key Expiration" dialog-title="Change Service Key Expiration"
dialog-action-title="Change Expiration"> dialog-action-title="Change Expiration">
<form> <form class="expiration-form">
<label>Expiration Date:</label>
<span class="datetime-picker" datetime="context.expirationChangeInfo.expiration_date"></span> <span class="datetime-picker" datetime="context.expirationChangeInfo.expiration_date"></span>
<span class="co-help-text"> <span class="co-help-text">
If specified, the date and time that the key expires. It is highly recommended to have an expiration date. If specified, the date and time that the key expires. It is highly recommended to have an expiration date.
@ -145,6 +212,19 @@
</form> </form>
</div> </div>
<!-- Delete Keys Confirm -->
<div class="cor-confirm-dialog"
dialog-context="deleteKeysInfo"
dialog-action="deleteKeys(deleteKeysInfo, callback)"
dialog-title="Delete Service Keys"
dialog-action-title="Delete Keys">
Are you <strong>sure</strong> you want to delete the follopwing service keys?<br>
All external services that use these keys for authentication will fail.
<ul class="keys-list">
<li ng-repeat="key in deleteKeysInfo.keys">{{ getKeyTitle(key) }}</li>
</ul>
</div>
<!-- Delete Key Confirm --> <!-- Delete Key Confirm -->
<div class="cor-confirm-dialog" <div class="cor-confirm-dialog"
dialog-context="deleteKeyInfo" dialog-context="deleteKeyInfo"
@ -155,6 +235,26 @@
All external services that use this key for authentication will fail. All external services that use this key for authentication will fail.
</div> </div>
<!-- Approve Keys Confirm -->
<div class="cor-confirm-dialog"
dialog-context="approveKeysInfo"
dialog-action="approveKeys(approveKeysInfo, callback)"
dialog-title="Approve Service Keys"
dialog-action-title="Approve Keys">
<form>
<div style="margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #eee;">
Approve the following service keys?
<ul class="keys-list">
<li ng-repeat="key in approveKeysInfo.keys">{{ getKeyTitle(key) }}</li>
</ul>
</div>
<div class="markdown-editor" content="approveKeysInfo.notes"></div>
<span class="co-help-text">
Enter optional notes for additional human-readable information about why the keys were approved.
</span>
</form>
</div>
<!-- Approve Key Confirm --> <!-- Approve Key Confirm -->
<div class="cor-confirm-dialog" <div class="cor-confirm-dialog"
dialog-context="approvalKeyInfo" dialog-context="approvalKeyInfo"

View file

@ -674,8 +674,8 @@ angular.module("core-ui", [])
}; };
this.checkByFilter = function(filter) { this.checkByFilter = function(filter) {
$scope.controller.checkByFilter(function(tag) { $scope.controller.checkByFilter(function(item) {
return filter({'tag': tag}); return filter({'item': item});
}); });
}; };
} }

View file

@ -11,13 +11,19 @@ angular.module('quay').directive('serviceKeysManager', function () {
scope: { scope: {
'isEnabled': '=isEnabled' 'isEnabled': '=isEnabled'
}, },
controller: function($scope, $element, ApiService, TableService) { controller: function($scope, $element, ApiService, TableService, UIService) {
$scope.options = { $scope.options = {
'filter': null, 'filter': null,
'predicate': 'expiration_datetime', 'predicate': 'expiration_datetime',
'reverse': false, 'reverse': false,
}; };
$scope.deleteKeysInfo = null;
$scope.approveKeysInfo = null;
$scope.changeKeysInfo = null;
$scope.checkedKeys = UIService.createCheckStateController([], 'kid');
$scope.TableService = TableService; $scope.TableService = TableService;
$scope.newKey = null; $scope.newKey = null;
$scope.creatingKey = false; $scope.creatingKey = false;
@ -48,6 +54,8 @@ angular.module('quay').directive('serviceKeysManager', function () {
$scope.orderedKeys = TableService.buildOrderedItems(keys, $scope.options, $scope.orderedKeys = TableService.buildOrderedItems(keys, $scope.options,
['name', 'kid', 'service'], ['name', 'kid', 'service'],
['creation_datetime', 'expiration_datetime']) ['creation_datetime', 'expiration_datetime'])
$scope.checkedKeys = UIService.createCheckStateController($scope.orderedKeys.visibleEntries, 'kid');
}; };
var loadServiceKeys = function() { var loadServiceKeys = function() {
@ -131,7 +139,10 @@ angular.module('quay').directive('serviceKeysManager', function () {
}; };
$scope.changeKeyExpiration = function(changeInfo, callback) { $scope.changeKeyExpiration = function(changeInfo, callback) {
var errorHandler = ApiService.errorDisplay('Could not change expiration on service key', callback); var errorHandler = ApiService.errorDisplay('Could not change expiration on service key', function() {
loadServiceKeys();
callback(false);
});
var data = { var data = {
'expiration': changeInfo.expiration_date 'expiration': changeInfo.expiration_date
@ -166,7 +177,10 @@ angular.module('quay').directive('serviceKeysManager', function () {
}; };
$scope.approveKey = function(approvalKeyInfo, callback) { $scope.approveKey = function(approvalKeyInfo, callback) {
var errorHandler = ApiService.errorDisplay('Could not approve service key', callback); var errorHandler = ApiService.errorDisplay('Could not approve service key', function() {
loadServiceKeys();
callback(false);
});
var data = { var data = {
'notes': approvalKeyInfo.notes 'notes': approvalKeyInfo.notes
@ -197,7 +211,10 @@ angular.module('quay').directive('serviceKeysManager', function () {
}; };
$scope.deleteKey = function(deleteKeyInfo, callback) { $scope.deleteKey = function(deleteKeyInfo, callback) {
var errorHandler = ApiService.errorDisplay('Could not delete service key', callback); var errorHandler = ApiService.errorDisplay('Could not delete service key', function() {
loadServiceKeys();
callback(false);
});
var params = { var params = {
'kid': deleteKeyInfo.key.kid 'kid': deleteKeyInfo.key.kid
@ -225,6 +242,132 @@ angular.module('quay').directive('serviceKeysManager', function () {
saveAs(blob, $scope.getKeyTitle(key) + '.pem'); saveAs(blob, $scope.getKeyTitle(key) + '.pem');
}; };
$scope.askDeleteMultipleKeys = function(keys) {
$scope.deleteKeysInfo = {
'keys': keys
};
};
$scope.askApproveMultipleKeys = function(keys) {
$scope.approveKeysInfo = {
'keys': keys
};
};
$scope.askChangeExpirationMultipleKeys = function(keys) {
$scope.changeKeysInfo = {
'keys': keys
};
};
$scope.allKeyFilter = function(key) {
return true;
};
$scope.noKeyFilter = function(key) {
return false;
};
$scope.unapprovedKeyFilter = function(key) {
return !key.approval;
};
$scope.expiredKeyFilter = function(key) {
return $scope.getExpirationInfo(key)['className'] == 'expired';
};
$scope.allRequireApproval = function(keys) {
for (var i = 0; i < keys.length; ++i) {
if (keys[i].approval) {
return false;
}
}
return true;
};
$scope.allExpired = function(keys) {
for (var i = 0; i < keys.length; ++i) {
if (!$scope.expiredKeyFilter(keys[i])) {
return false;
}
}
return true;
};
var forAllKeys = function(keys, error_msg, performer, callback) {
var counter = 0;
var performAction = function() {
if (counter >= keys.length) {
loadServiceKeys();
callback(true);
return;
}
var key = keys[counter];
var errorHandler = function(resp) {
if (resp.status != 404) {
bootbox.alert(error_msg);
loadServiceKeys();
callback(false);
return;
}
performAction();
};
counter++;
performer(key).then(performAction, errorHandler);
};
performAction();
};
$scope.deleteKeys = function(info, callback) {
var performer = function(key) {
var params = {
'kid': key.kid
};
return ApiService.deleteServiceKey(null, params);
};
forAllKeys(info.keys, 'Could not delete service key', performer, callback);
};
$scope.approveKeys = function(info, callback) {
var performer = function(key) {
var params = {
'kid': key.kid
};
var data = {
'notes': $scope.approveKeysInfo.notes
};
return ApiService.approveServiceKey(data, params);
};
forAllKeys(info.keys, 'Could not approve service key', performer, callback);
};
$scope.changeKeysExpiration = function(info, callback) {
var performer = function(key) {
var data = {
'expiration': info.expiration_date || null
};
var params = {
'kid': key.kid
};
return ApiService.updateServiceKey(data, params);
};
forAllKeys(info.keys, 'Could not update service key', performer, callback);
};
$scope.$watch('options.filter', buildOrderedKeys); $scope.$watch('options.filter', buildOrderedKeys);
$scope.$watch('options.predicate', buildOrderedKeys); $scope.$watch('options.predicate', buildOrderedKeys);
$scope.$watch('options.reverse', buildOrderedKeys); $scope.$watch('options.reverse', buildOrderedKeys);