Merge branch 'master' into comewithmeifyouwanttowork

This commit is contained in:
Joseph Schorr 2014-08-28 20:50:13 -04:00
commit 3b72b26836
62 changed files with 923 additions and 714 deletions

View file

@ -473,6 +473,22 @@ i.toggle-icon:hover {
.docker-auth-dialog .token-dialog-body .well {
margin-bottom: 0px;
position: relative;
padding-right: 24px;
}
.docker-auth-dialog .token-dialog-body .well i.fa-refresh {
position: absolute;
top: 9px;
right: 9px;
font-size: 20px;
color: gray;
transition: all 0.5s ease-in-out;
cursor: pointer;
}
.docker-auth-dialog .token-dialog-body .well i.fa-refresh:hover {
color: black;
}
.docker-auth-dialog .token-view {
@ -738,7 +754,7 @@ i.toggle-icon:hover {
}
.user-notification.notification-animated {
width: 21px;
min-width: 21px;
transform: scale(0);
-moz-transform: scale(0);
@ -832,7 +848,7 @@ i.toggle-icon:hover {
background-color: red;
}
.phase-icon.waiting, .phase-icon.starting, .phase-icon.initializing {
.phase-icon.waiting, .phase-icon.unpacking, .phase-icon.starting, .phase-icon.initializing {
background-color: #ddd;
}
@ -2266,6 +2282,14 @@ p.editable:hover i {
position: relative;
}
.copy-box-element.disabled .input-group-addon {
display: none;
}
.copy-box-element.disabled input {
border-radius: 4px !important;
}
.global-zeroclipboard-container embed {
cursor: pointer;
}

View file

@ -1,4 +1,4 @@
<div class="copy-box-element">
<div class="copy-box-element" ng-class="disabled ? 'disabled' : ''">
<div class="id-container">
<div class="input-group">
<input type="text" class="form-control" value="{{ value }}" readonly>

View file

@ -10,19 +10,33 @@
</div>
<div class="modal-body token-dialog-body">
<div class="alert alert-info">The docker <u>username</u> is <b>{{ username }}</b> and the <u>password</u> is the token below. You may use any value for email.</div>
<div class="well well-sm">
<div class="well well-sm" ng-show="regenerating">
Regenerating Token...
<i class="fa fa-refresh fa-spin"></i>
</div>
<div class="well well-sm" ng-show="!regenerating">
<input id="token-view" class="token-view" type="text" value="{{ token }}" onClick="this.select();" readonly>
<i class="fa fa-refresh" ng-show="supportsRegenerate" ng-click="askRegenerate()"
data-title="Regenerate Token"
data-placement="left"
bs-tooltip></i>
</div>
</div>
<div class="modal-footer">
<div class="modal-footer" ng-show="regenerating">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
<div class="modal-footer" ng-show="!regenerating">
<span class="download-cfg" ng-show="isDownloadSupported()">
<i class="fa fa-download"></i>
<a href="javascript:void(0)" ng-click="downloadCfg(shownRobot)">Download .dockercfg file</a>
</span>
<div id="clipboardCopied" style="display: none">
Copied to clipboard
<div class="clipboard-copied-message" style="display: none">
Copied
</div>
<button id="copyClipboard" type="button" class="btn btn-primary" data-clipboard-target="token-view">Copy to clipboard</button>
<input type="hidden" name="command-data" id="command-data" value="{{ command }}">
<button id="copyClipboard" type="button" class="btn btn-primary" data-clipboard-target="command-data">Copy Login Command</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->

View file

@ -37,15 +37,7 @@
<a href="javascript:void(0)" class="dropdown-toggle user-dropdown user-view" data-toggle="dropdown">
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=32&d=identicon" />
{{ user.username }}
<span class="badge user-notification notification-animated"
ng-show="notificationService.notifications.length"
ng-class="notificationService.notificationClasses"
bs-tooltip=""
data-title="User Notifications"
data-placement="left"
data-container="body">
{{ notificationService.notifications.length }}
</span>
<span class="notifications-bubble"></span>
<b class="caret"></b>
</a>
<ul class="dropdown-menu">
@ -58,11 +50,7 @@
<a href="javascript:void(0)" data-template="/static/directives/notification-bar.html"
data-animation="am-slide-right" bs-aside="aside" data-container="body">
Notifications
<span class="badge user-notification"
ng-class="notificationService.notificationClasses"
ng-show="notificationService.notifications.length">
{{ notificationService.notifications.length }}
</span>
<span class="notifications-bubble"></span>
</a>
</li>
<li><a ng-href="/organizations/" target="{{ appLinkTarget() }}">Organizations</a></li>

View file

@ -3,7 +3,10 @@
<div class="aside-content">
<div class="aside-header">
<button type="button" class="close" ng-click="$hide()">&times;</button>
<h4 class="aside-title">Notifications</h4>
<h4 class="aside-title">
Notifications
<span class="notifications-bubble"></span>
</h4>
</div>
<div class="aside-body">
<div ng-repeat="notification in notificationService.notifications">

View file

@ -0,0 +1,7 @@
<span class="notifications-bubble-element">
<span class="badge user-notification notification-animated"
ng-show="notificationService.notifications.length"
ng-class="notificationService.notificationClasses">
{{ notificationService.notifications.length }}<span ng-if="notificationService.additionalNotifications">+</span>
</span>
</span>

View file

@ -31,7 +31,7 @@
</div>
<div class="docker-auth-dialog" username="shownRobot.name" token="shownRobot.token"
shown="!!shownRobot" counter="showRobotCounter">
shown="!!shownRobot" counter="showRobotCounter" supports-regenerate="true" regenerate="regenerateToken(username)">
<i class="fa fa-wrench"></i> {{ shownRobot.name }}
</div>
</div>

View file

@ -1,6 +1,46 @@
var TEAM_PATTERN = '^[a-zA-Z][a-zA-Z0-9]+$';
var ROBOT_PATTERN = '^[a-zA-Z][a-zA-Z0-9]{3,29}$';
$.fn.clipboardCopy = function() {
if (zeroClipboardSupported) {
(new ZeroClipboard($(this)));
return true;
}
this.hide();
return false;
};
var zeroClipboardSupported = true;
ZeroClipboard.config({
'swfPath': 'static/lib/ZeroClipboard.swf'
});
ZeroClipboard.on("error", function(e) {
zeroClipboardSupported = false;
});
ZeroClipboard.on('aftercopy', function(e) {
var container = e.target.parentNode.parentNode.parentNode;
var message = $(container).find('.clipboard-copied-message')[0];
// Resets the animation.
var elem = message;
elem.style.display = 'none';
elem.classList.remove('animated');
// Show the notification.
setTimeout(function() {
elem.style.display = 'inline-block';
elem.classList.add('animated');
}, 10);
// Reset the notification.
setTimeout(function() {
elem.style.display = 'none';
}, 5000);
});
function getRestUrl(args) {
var url = '';
for (var i = 0; i < arguments.length; ++i) {
@ -59,18 +99,8 @@ function getFirstTextLine(commentString) {
}
function createRobotAccount(ApiService, is_org, orgname, name, callback) {
ApiService.createRobot(is_org ? orgname : null, null, {'robot_shortname': name}).then(callback, function(resp) {
bootbox.dialog({
"message": resp.data ? resp.data['message'] : 'The robot account could not be created',
"title": "Cannot create robot account",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
ApiService.createRobot(is_org ? orgname : null, null, {'robot_shortname': name})
.then(callback, ApiService.errorDisplay('Cannot create robot account'));
}
function createOrganizationTeam(ApiService, orgname, teamname, callback) {
@ -84,18 +114,8 @@ function createOrganizationTeam(ApiService, orgname, teamname, callback) {
'teamname': teamname
};
ApiService.updateOrganizationTeam(data, params).then(callback, function(resp) {
bootbox.dialog({
"message": resp.data ? resp.data : 'The team could not be created',
"title": "Cannot create team",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
ApiService.updateOrganizationTeam(data, params)
.then(callback, ApiService.errorDisplay('Cannot create team'));
}
function getMarkedDown(string) {
@ -870,6 +890,38 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
buildMethodsForEndpointResource(endpointResource, resourceMap);
}
apiService.getErrorMessage = function(resp, defaultMessage) {
var message = defaultMessage;
if (resp['data']) {
message = resp['data']['error_message'] || resp['data']['message'] || resp['data']['error_description'] || message;
}
return message;
};
apiService.errorDisplay = function(defaultMessage, opt_handler) {
return function(resp) {
var message = apiService.getErrorMessage(resp, defaultMessage);
if (opt_handler) {
var handlerMessage = opt_handler(resp);
if (handlerMessage) {
message = handlerMessage;
}
}
bootbox.dialog({
"message": message,
"title": defaultMessage,
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
};
};
return apiService;
}]);
@ -1126,7 +1178,8 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
'user': null,
'notifications': [],
'notificationClasses': [],
'notificationSummaries': []
'notificationSummaries': [],
'additionalNotifications': false
};
var pollTimerHandle = null;
@ -1244,7 +1297,9 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
'uuid': notification.id
};
ApiService.updateUserNotification(notification, params);
ApiService.updateUserNotification(notification, params, function() {
notificationService.update();
}, ApiService.errorDisplay('Could not update notification'));
var index = $.inArray(notification, notificationService.notifications);
if (index >= 0) {
@ -1310,6 +1365,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
ApiService.listUserNotifications().then(function(resp) {
notificationService.notifications = resp['notifications'];
notificationService.additionalNotifications = resp['additional'];
notificationService.notificationClasses = notificationService.getClasses(notificationService.notifications);
});
};
@ -1541,7 +1597,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
});
};
planService.changePlan = function($scope, orgname, planId, callbacks) {
planService.changePlan = function($scope, orgname, planId, callbacks, opt_async) {
if (!Features.BILLING) { return; }
if (callbacks['started']) {
@ -1554,7 +1610,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
planService.getCardInfo(orgname, function(cardInfo) {
if (plan.price > 0 && (previousSubscribeFailure || !cardInfo.last4)) {
var title = cardInfo.last4 ? 'Subscribe' : 'Start Trial ({{amount}} plan)';
planService.showSubscribeDialog($scope, orgname, planId, callbacks, title);
planService.showSubscribeDialog($scope, orgname, planId, callbacks, title, /* async */true);
return;
}
@ -1627,9 +1683,34 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
return email;
};
planService.showSubscribeDialog = function($scope, orgname, planId, callbacks, opt_title) {
planService.showSubscribeDialog = function($scope, orgname, planId, callbacks, opt_title, opt_async) {
if (!Features.BILLING) { return; }
// If the async parameter is true and this is a browser that does not allow async popup of the
// Stripe dialog (such as Mobile Safari or IE), show a bootbox to show the dialog instead.
var isIE = navigator.appName.indexOf("Internet Explorer") != -1;
var isMobileSafari = navigator.userAgent.match(/(iPod|iPhone|iPad)/) && navigator.userAgent.match(/AppleWebKit/);
if (opt_async && (isIE || isMobileSafari)) {
bootbox.dialog({
"message": "Please click 'Subscribe' to continue",
"buttons": {
"subscribe": {
"label": "Subscribe",
"className": "btn-primary",
"callback": function() {
planService.showSubscribeDialog($scope, orgname, planId, callbacks, opt_title, false);
}
},
"close": {
"label": "Cancel",
"className": "btn-default"
}
}
});
return;
}
if (callbacks['opening']) {
callbacks['opening']();
}
@ -2084,18 +2165,7 @@ quayApp.directive('applicationReference', function () {
template: '/static/directives/application-reference-dialog.html',
show: true
});
}, function() {
bootbox.dialog({
"message": 'The application could not be found; it might have been deleted.',
"title": "Cannot find application",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
}, ApiService.errorDisplay('Application could not be found'));
};
}
};
@ -2176,6 +2246,8 @@ quayApp.directive('copyBox', function () {
'hoveringMessage': '=hoveringMessage'
},
controller: function($scope, $element, $rootScope) {
$scope.disabled = false;
var number = $rootScope.__copyBoxIdCounter || 0;
$rootScope.__copyBoxIdCounter = number + 1;
$scope.inputId = "copy-box-input-" + number;
@ -2185,27 +2257,7 @@ quayApp.directive('copyBox', function () {
input.attr('id', $scope.inputId);
button.attr('data-clipboard-target', $scope.inputId);
var clip = new ZeroClipboard($(button), { 'moviePath': 'static/lib/ZeroClipboard.swf' });
clip.on('complete', function(e) {
var message = $(this.parentNode.parentNode.parentNode).find('.clipboard-copied-message')[0];
// Resets the animation.
var elem = message;
elem.style.display = 'none';
elem.classList.remove('animated');
// Show the notification.
setTimeout(function() {
elem.style.display = 'inline-block';
elem.classList.add('animated');
}, 10);
// Reset the notification.
setTimeout(function() {
elem.style.display = 'none';
}, 5000);
});
$scope.disabled = !button.clipboardCopy();
}
};
return directiveDefinitionObject;
@ -2439,11 +2491,38 @@ quayApp.directive('dockerAuthDialog', function (Config) {
'username': '=username',
'token': '=token',
'shown': '=shown',
'counter': '=counter'
'counter': '=counter',
'supportsRegenerate': '@supportsRegenerate',
'regenerate': '&regenerate'
},
controller: function($scope, $element) {
controller: function($scope, $element) {
var updateCommand = function() {
$scope.command = 'docker login -e="." -u="' + $scope.username +
'" -p="' + $scope.token + '" ' + Config['SERVER_HOSTNAME'];
};
$scope.$watch('username', updateCommand);
$scope.$watch('token', updateCommand);
$scope.regenerating = true;
$scope.askRegenerate = function() {
bootbox.confirm('Are you sure you want to regenerate the token? All existing login credentials will become invalid', function(resp) {
if (resp) {
$scope.regenerating = true;
$scope.regenerate({'username': $scope.username, 'token': $scope.token});
}
});
};
$scope.isDownloadSupported = function() {
try { return !!new Blob(); } catch(e){}
var isSafari = /^((?!chrome).)*safari/i.test(navigator.userAgent);
if (isSafari) {
// Doesn't work properly in Safari, sadly.
return false;
}
try { return !!new Blob(); } catch(e) {}
return false;
};
@ -2461,6 +2540,8 @@ quayApp.directive('dockerAuthDialog', function (Config) {
};
var show = function(r) {
$scope.regenerating = false;
if (!$scope.shown || !$scope.username || !$scope.token) {
$('#dockerauthmodal').modal('hide');
return;
@ -2706,6 +2787,8 @@ quayApp.directive('logsView', function () {
return 'Delete notification of event "' + eventData['title'] + '" for repository {repo}';
},
'regenerate_robot_token': 'Regenerated token for robot {robot}',
// Note: These are deprecated.
'add_repo_webhook': 'Add webhook in repository {repo}',
'delete_repo_webhook': 'Delete webhook in repository {repo}'
@ -2752,6 +2835,7 @@ quayApp.directive('logsView', function () {
'reset_application_client_secret': 'Reset Client Secret',
'add_repo_notification': 'Add repository notification',
'delete_repo_notification': 'Delete repository notification',
'regenerate_robot_token': 'Regenerate Robot Token',
// Note: these are deprecated.
'add_repo_webhook': 'Add webhook',
@ -2878,18 +2962,7 @@ quayApp.directive('applicationManager', function () {
ApiService.createOrganizationApplication(data, params).then(function(resp) {
$scope.applications.push(resp);
}, function(resp) {
bootbox.dialog({
"message": resp['message'] || 'The application could not be created',
"title": "Cannot create application",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
}, ApiService.errorDisplay('Cannot create application'));
};
var update = function() {
@ -2934,6 +3007,20 @@ quayApp.directive('robotsManager', function () {
$scope.shownRobot = null;
$scope.showRobotCounter = 0;
$scope.regenerateToken = function(username) {
if (!username) { return; }
var shortName = $scope.getShortenedName(username);
ApiService.regenerateRobotToken($scope.organization, null, {'robot_shortname': shortName}).then(function(updated) {
var index = $scope.findRobotIndexByName(username);
if (index >= 0) {
$scope.robots.splice(index, 1);
$scope.robots.push(updated);
}
$scope.shownRobot = updated;
}, ApiService.errorDisplay('Cannot regenerate robot account token'));
};
$scope.showRobot = function(info) {
$scope.shownRobot = info;
$scope.showRobotCounter++;
@ -2974,18 +3061,7 @@ quayApp.directive('robotsManager', function () {
if (index >= 0) {
$scope.robots.splice(index, 1);
}
}, function() {
bootbox.dialog({
"message": 'The selected robot account could not be deleted',
"title": "Cannot delete robot account",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
}, ApiService.errorDisplay('Cannot delete robot account'));
};
var update = function() {
@ -3050,18 +3126,7 @@ quayApp.directive('prototypeManager', function () {
ApiService.updateOrganizationPrototypePermission(data, params).then(function(resp) {
prototype.role = role;
}, function(resp) {
bootbox.dialog({
"message": resp.data ? resp.data : 'The permission could not be modified',
"title": "Cannot modify permission",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
}, ApiService.errorDisplay('Cannot modify permission'));
};
$scope.comparePrototypes = function(p) {
@ -3101,23 +3166,16 @@ quayApp.directive('prototypeManager', function () {
data['activating_user'] = $scope.activatingForNew;
}
var errorHandler = ApiService.errorDisplay('Cannot create permission',
function(resp) {
$('#addPermissionDialogModal').modal('hide');
});
ApiService.createOrganizationPrototypePermission(data, params).then(function(resp) {
$scope.prototypes.push(resp);
$scope.loading = false;
$('#addPermissionDialogModal').modal('hide');
}, function(resp) {
$('#addPermissionDialogModal').modal('hide');
bootbox.dialog({
"message": resp.data ? resp.data : 'The permission could not be created',
"title": "Cannot create permission",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
}, errorHandler);
};
$scope.deletePrototype = function(prototype) {
@ -3131,18 +3189,7 @@ quayApp.directive('prototypeManager', function () {
ApiService.deleteOrganizationPrototypePermission(null, params).then(function(resp) {
$scope.prototypes.splice($scope.prototypes.indexOf(prototype), 1);
$scope.loading = false;
}, function(resp) {
bootbox.dialog({
"message": resp.data ? resp.data : 'The permission could not be deleted',
"title": "Cannot delete permission",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
}, ApiService.errorDisplay('Cannot delete permission'));
};
var update = function() {
@ -3990,7 +4037,7 @@ quayApp.directive('planManager', function () {
return true;
};
$scope.changeSubscription = function(planId) {
$scope.changeSubscription = function(planId, opt_async) {
if ($scope.planChanging) { return; }
var callbacks = {
@ -4004,7 +4051,7 @@ quayApp.directive('planManager', function () {
}
};
PlanService.changePlan($scope, $scope.organization, planId, callbacks);
PlanService.changePlan($scope, $scope.organization, planId, callbacks, opt_async);
};
$scope.cancelSubscription = function() {
@ -4067,7 +4114,7 @@ quayApp.directive('planManager', function () {
if ($scope.readyForPlan) {
var planRequested = $scope.readyForPlan();
if (planRequested && planRequested != PlanService.getFreePlan()) {
$scope.changeSubscription(planRequested);
$scope.changeSubscription(planRequested, /* async */true);
}
}
});
@ -4485,26 +4532,17 @@ quayApp.directive('setupTriggerDialog', function () {
$scope.activating = true;
var errorHandler = ApiService.errorDisplay('Cannot activate build trigger', function(resp) {
$scope.hide();
$scope.canceled({'trigger': $scope.trigger});
});
ApiService.activateBuildTrigger(data, params).then(function(resp) {
$scope.hide();
$scope.trigger['is_active'] = true;
$scope.trigger['pull_robot'] = resp['pull_robot'];
$scope.activated({'trigger': $scope.trigger});
}, function(resp) {
$scope.hide();
$scope.canceled({'trigger': $scope.trigger});
bootbox.dialog({
"message": resp['data']['message'] || 'The build trigger setup could not be completed',
"title": "Could not activate build trigger",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
}, errorHandler);
};
var check = function() {
@ -4844,6 +4882,9 @@ quayApp.directive('buildMessage', function () {
case 'waiting':
return 'Waiting for available build worker';
case 'unpacking':
return 'Unpacking build package';
case 'pulling':
return 'Pulling base image';
@ -4899,6 +4940,7 @@ quayApp.directive('buildProgress', function () {
case 'starting':
case 'waiting':
case 'cannot_load':
case 'unpacking':
return 0;
break;
}
@ -5118,6 +5160,23 @@ quayApp.directive('twitterView', function () {
});
quayApp.directive('notificationsBubble', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/notifications-bubble.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
},
controller: function($scope, UserService, NotificationService) {
$scope.notificationService = NotificationService;
}
};
return directiveDefinitionObject;
});
quayApp.directive('notificationView', function () {
var directiveDefinitionObject = {
priority: 0,
@ -5705,8 +5764,8 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
}
}
if (!Features.BILLING && response.status == 402) {
$('#overlicenseModal').modal({});
if (response.status == 503) {
$('#cannotContactService').modal({});
return false;
}

View file

@ -1,34 +1,4 @@
$.fn.clipboardCopy = function() {
var clip = new ZeroClipboard($(this), { 'moviePath': 'static/lib/ZeroClipboard.swf' });
clip.on('complete', function() {
// Resets the animation.
var elem = $('#clipboardCopied')[0];
if (!elem) {
return;
}
elem.style.display = 'none';
elem.classList.remove('animated');
// Show the notification.
setTimeout(function() {
if (!elem) { return; }
elem.style.display = 'inline-block';
elem.classList.add('animated');
}, 10);
});
};
function SignInCtrl($scope, $location) {
var redirect = $location.search()['redirect'];
if (redirect && redirect.indexOf('/') < 0) {
delete $location.search()['redirect'];
$scope.redirectUrl = '/' + redirect;
return;
}
$scope.redirectUrl = '/';
}
function GuideCtrl() {
@ -543,23 +513,15 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
'image': image.id
};
var errorHandler = ApiService.errorDisplay('Cannot create or move tag', function(resp) {
$('#addTagModal').modal('hide');
});
ApiService.changeTagImage(data, params).then(function(resp) {
$scope.creatingTag = false;
loadViewInfo();
$('#addTagModal').modal('hide');
}, function(resp) {
$('#addTagModal').modal('hide');
bootbox.dialog({
"message": resp.data ? resp.data : 'Could not create or move tag',
"title": "Cannot create or move tag",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
}, errorHandler);
};
$scope.deleteTag = function(tagName) {
@ -573,18 +535,7 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
ApiService.deleteFullTag(null, params).then(function() {
loadViewInfo();
}, function(resp) {
bootbox.dialog({
"message": resp.data ? resp.data : 'Could not delete tag',
"title": "Cannot delete tag",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
}, ApiService.errorDisplay('Cannot delete tag'));
};
$scope.getImagesForTagBySize = function(tag) {
@ -763,8 +714,6 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
// Load the builds for this repository. If none are active it will cancel the poll.
startBuildInfoTimer(repo);
$('#copyClipboard').clipboardCopy();
});
};
@ -1373,17 +1322,16 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
};
$scope.deleteRole = function(entityName, kind) {
var errorHandler = ApiService.errorDisplay('Cannot change permission', function(resp) {
if (resp.status == 409) {
return 'Cannot change permission as you do not have the authority';
}
});
var permissionDelete = Restangular.one(getRestUrl('repository', namespace, name, 'permissions', kind, entityName));
permissionDelete.customDELETE().then(function() {
delete $scope.permissions[kind][entityName];
}, function(resp) {
if (resp.status == 409) {
$scope.changePermError = resp.data || '';
$('#channgechangepermModal').modal({});
} else {
$('#cannotchangeModal').modal({});
}
});
}, errorHandler);
};
$scope.addRole = function(entityName, role, kind) {
@ -1394,9 +1342,7 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
var permissionPost = Restangular.one(getRestUrl('repository', namespace, name, 'permissions', kind, entityName));
permissionPost.customPUT(permission).then(function(result) {
$scope.permissions[kind][entityName] = result;
}, function(result) {
$('#cannotchangeModal').modal({});
});
}, ApiService.errorDisplay('Cannot change permission'));
};
$scope.roles = [
@ -1611,18 +1557,7 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
window.console.log(resp);
var url = '/repository/' + namespace + '/' + name + '/build?current=' + resp['id'];
document.location = url;
}, function(resp) {
bootbox.dialog({
"message": resp['message'] || 'The build could not be started',
"title": "Could not start build",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
}, ApiService.errorDisplay('Could not start build'));
};
$scope.deleteTrigger = function(trigger) {
@ -1750,18 +1685,7 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
ApiService.deleteUserAuthorization(null, params).then(function(resp) {
$scope.authorizedApps.splice($scope.authorizedApps.indexOf(accessTokenInfo), 1);
}, function(resp) {
bootbox.dialog({
"message": resp.message || 'Could not revoke authorization',
"title": "Cannot revoke authorization",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
}, ApiService.errorDisplay('Could not revoke authorization'));
};
$scope.loadLogs = function() {
@ -1770,7 +1694,6 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
};
$scope.loadInvoices = function() {
if (!$scope.hasPaidBusinessPlan) { return; }
$scope.invoicesShown++;
};
@ -1956,9 +1879,6 @@ function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, I
// Fetch the image's changes.
fetchChanges();
$('#copyClipboard').clipboardCopy();
return image;
});
};
@ -2226,13 +2146,14 @@ function OrgViewCtrl($rootScope, $scope, ApiService, $routeParams) {
'teamname': teamname
};
var errorHandler = ApiService.errorDisplay('Cannot delete team', function() {
$scope.currentDeleteTeam = null;
});
ApiService.deleteOrganizationTeam(null, params).then(function() {
delete $scope.organization.teams[teamname];
$scope.currentDeleteTeam = null;
}, function() {
$('#cannotchangeModal').modal({});
$scope.currentDeleteTeam = null;
});
}, errorHandler);
};
var loadOrganization = function() {
@ -2575,9 +2496,9 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan
};
PlanService.changePlan($scope, org.name, $scope.holder.currentPlan.stripeId, callbacks);
}, function(result) {
}, function(resp) {
$scope.creating = false;
$scope.createError = result.data.error_description || result.data;
$scope.createError = ApiService.getErrorMessage(resp);
$timeout(function() {
$('#orgName').popover('show');
});
@ -2654,18 +2575,7 @@ function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $tim
$timeout(function() {
$location.path('/organization/' + orgname + '/admin');
}, 500);
}, function(resp) {
bootbox.dialog({
"message": resp.message || 'Could not delete application',
"title": "Cannot delete application",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
}, ApiService.errorDisplay('Could not delete application'));
};
$scope.updateApplication = function() {
@ -2683,22 +2593,13 @@ function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $tim
delete $scope.application['gravatar_email'];
}
var errorHandler = ApiService.errorDisplay('Could not update application', function(resp) {
$scope.updating = false;
});
ApiService.updateOrganizationApplication($scope.application, params).then(function(resp) {
$scope.application = resp;
$scope.updating = false;
}, function(resp) {
$scope.updating = false;
bootbox.dialog({
"message": resp.message || 'Could not update application',
"title": "Cannot update application",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
}, errorHandler);
};
$scope.resetClientSecret = function() {
@ -2711,18 +2612,7 @@ function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $tim
ApiService.resetOrganizationApplicationClientSecret(null, params).then(function(resp) {
$scope.application = resp;
}, function(resp) {
bootbox.dialog({
"message": resp.message || 'Could not reset client secret',
"title": "Cannot reset client secret",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
}, ApiService.errorDisplay('Could not reset client secret'));
};
var loadOrganization = function() {
@ -2818,18 +2708,7 @@ function SuperUserAdminCtrl($scope, ApiService, Features, UserService) {
ApiService.changeInstallUser(data, params).then(function(resp) {
$scope.loadUsersInternal();
}, function(resp) {
bootbox.dialog({
"message": resp.data ? resp.data.message : 'Could not change user',
"title": "Cannot change user",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
}, ApiService.errorDisplay('Could not change user'));
};
$scope.deleteUser = function(user) {
@ -2841,49 +2720,10 @@ function SuperUserAdminCtrl($scope, ApiService, Features, UserService) {
ApiService.deleteInstallUser(null, params).then(function(resp) {
$scope.loadUsersInternal();
}, function(resp) {
bootbox.dialog({
"message": resp.data ? resp.data.message : 'Could not delete user',
"title": "Cannot delete user",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
}, ApiService.errorDisplay('Cannot delete user'));
};
var seatUsageLoaded = function(usage) {
$scope.usageLoading = false;
if (usage.count > usage.allowed) {
$scope.limit = 'over';
} else if (usage.count == usage.allowed) {
$scope.limit = 'at';
} else if (usage.count >= usage.allowed * 0.7) {
$scope.limit = 'near';
} else {
$scope.limit = 'none';
}
if (!$scope.chart) {
$scope.chart = new UsageChart();
$scope.chart.draw('seat-usage-chart');
}
$scope.chart.update(usage.count, usage.allowed);
};
var loadSeatUsage = function() {
$scope.usageLoading = true;
ApiService.getSeatCount().then(function(resp) {
seatUsageLoaded(resp);
});
};
loadSeatUsage();
$scope.loadUsers();
}
function TourCtrl($scope, $location) {
@ -2913,4 +2753,4 @@ function ConfirmInviteCtrl($scope, $location, UserService, ApiService, Notificat
});
$scope.redirectUrl = 'confirminvite?code=' + $location.search()['code'];
}
}

View file

@ -148,6 +148,8 @@ ImageHistoryTree.prototype.updateDimensions_ = function() {
var ch = dimensions.ch;
// Set the height of the container so that it never goes offscreen.
if (!$('#' + container).removeOverscroll) { return; }
$('#' + container).removeOverscroll();
var viewportHeight = $(window).height();
var boundingBox = document.getElementById(container).getBoundingClientRect();

17
static/lib/ZeroClipboard.min.js vendored Executable file → Normal file

File diff suppressed because one or more lines are too long

BIN
static/lib/ZeroClipboard.swf Executable file → Normal file

Binary file not shown.

View file

@ -53,7 +53,7 @@
<label for="orgName">Organization Name</label>
<input id="orgName" name="orgName" type="text" class="form-control" placeholder="Organization Name"
ng-model="org.name" required autofocus data-trigger="manual" data-content="{{ createError }}"
data-placement="right" data-container="body" ng-pattern="/^[a-z0-9_]{4,30}$/">
data-placement="bottom" data-container="body" ng-pattern="/^[a-z0-9_]{4,30}$/">
<span class="description">This will also be the namespace for your repositories</span>
</div>

View file

@ -34,6 +34,13 @@
</span>
<i class="fa fa-upload visible-lg"></i>
</div>
<div class="feature">
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
data-title="Administrators can view and download the full invoice history for their organization">
Invoice History
</span>
<i class="fa fa-calendar visible-lg"></i>
</div>
<div class="feature">
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
data-title="Grant subsets of users in an organization their own permissions, either on a global basis or a per-repository basis">
@ -48,13 +55,6 @@
</span>
<i class="fa fa-bar-chart-o visible-lg"></i>
</div>
<div class="feature">
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
data-title="Administrators can view and download the full invoice history for their organization">
Invoice History
</span>
<i class="fa fa-calendar visible-lg"></i>
</div>
<div class="feature">
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
data-title="All plans have a free trial">
@ -81,7 +81,7 @@
<div class="feature present"></div>
<div class="feature present"></div>
<div class="feature present"></div>
<div class="feature" ng-class="plan.bus_features ? 'present' : ''"></div>
<div class="feature present"></div>
<div class="feature" ng-class="plan.bus_features ? 'present' : ''"></div>
<div class="feature" ng-class="plan.bus_features ? 'present' : ''"></div>
<div class="feature present"></div>
@ -93,9 +93,9 @@
<div class="feature present">SSL Encryption</div>
<div class="feature present">Robot accounts</div>
<div class="feature present">Dockerfile Build</div>
<div class="feature present">Invoice History</div>
<div class="feature" ng-class="plan.bus_features ? 'present' : 'notpresent'">Teams</div>
<div class="feature" ng-class="plan.bus_features ? 'present' : 'notpresent'">Logging</div>
<div class="feature" ng-class="plan.bus_features ? 'present' : 'notpresent'">Invoice History</div>
<div class="feature present">Free Trial</div>
</div>

View file

@ -18,7 +18,8 @@
<div class="col-md-2">
<ul class="nav nav-pills nav-stacked">
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#permissions">Permissions</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#trigger" ng-click="loadTriggers()">Build Triggers</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#trigger" ng-click="loadTriggers()"
quay-require="['BUILD_SUPPORT']">Build Triggers</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#badge">Status Badge</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#notification" ng-click="loadNotifications()">Notifications</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#publicprivate">Public/Private</a></li>
@ -225,7 +226,7 @@
</div>
<!-- Triggers tab -->
<div id="trigger" class="tab-pane">
<div id="trigger" class="tab-pane" quay-require="['BUILD_SUPPORT']">
<div class="panel panel-default">
<div class="panel-heading">Build Triggers
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="Triggers from various services (such as GitHub) which tell the repository to be built and updated."></i>
@ -377,24 +378,6 @@
counter="showNewNotificationCounter"
notification-created="handleNotificationCreated(notification)"></div>
<!-- Modal message dialog -->
<div class="modal fade" id="cannotchangeModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Cannot change</h4>
</div>
<div class="modal-body">
The selected action could not be performed because you do not have that authority.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="makepublicModal">
<div class="modal-dialog">
@ -441,26 +424,6 @@
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="channgechangepermModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Cannot change permissions</h4>
</div>
<div class="modal-body">
<span ng-show="!changePermError">You do not have permission to change the permissions on the repository.</span>
<span ng-show="changePermError">{{ changePermError }}</span>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="confirmdeleteModal">
<div class="modal-dialog">

View file

@ -8,9 +8,6 @@
<div class="col-md-2">
<ul class="nav nav-pills nav-stacked">
<li class="active">
<a href="javascript:void(0)" data-toggle="tab" data-target="#license">License and Usage</a>
</li>
<li>
<a href="javascript:void(0)" data-toggle="tab" data-target="#users" ng-click="loadUsers()">Users</a>
</li>
</ul>
@ -19,19 +16,8 @@
<!-- Content -->
<div class="col-md-10">
<div class="tab-content">
<!-- License tab -->
<div id="license" class="tab-pane active">
<div class="quay-spinner 3x" ng-show="usageLoading"></div>
<!-- Chart -->
<div>
<div id="seat-usage-chart" class="usage-chart limit-{{limit}}"></div>
<span class="usage-caption" ng-show="chart">Seat Usage</span>
</div>
</div>
<!-- Users tab -->
<div id="users" class="tab-pane">
<div id="users" class="tab-pane active">
<div class="quay-spinner" ng-show="!users"></div>
<div class="alert alert-error" ng-show="usersError">
{{ usersError }}

View file

@ -25,7 +25,7 @@
<li ng-show="hasPaidPlan" quay-require="['BILLING']">
<a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing Options</a>
</li>
<li ng-show="hasPaidBusinessPlan" quay-require="['BILLING']">
<li ng-show="hasPaidPlan" quay-require="['BILLING']">
<a href="javascript:void(0)" data-toggle="tab" data-target="#billing" ng-click="loadInvoices()">Billing History</a>
</li>

View file

@ -18,7 +18,7 @@
<div class="dropdown" data-placement="top" style="display: inline-block"
bs-tooltip=""
data-title="{{ runningBuilds.length ? 'Dockerfile Builds Running: ' + (runningBuilds.length) : 'Dockerfile Build' }}"
ng-show="repo.can_write || buildHistory.length">
quay-show="Features.BUILD_SUPPORT && (repo.can_write || buildHistory.length)">
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-tasks fa-lg"></i>
<span class="count" ng-class="runningBuilds.length ? 'visible' : ''"><span>{{ runningBuilds.length ? runningBuilds.length : '' }}</span></span>
@ -58,16 +58,9 @@
<span class="pull-command visible-md-inline">
<div class="pull-container" data-title="Pull repository" bs-tooltip="tooltip.title">
<div class="input-group">
<input id="pull-text" type="text" class="form-control" value="{{ 'docker pull ' + Config.getDomain() + '/' + repo.namespace + '/' + repo.name }}" readonly>
<span id="copyClipboard" class="input-group-addon" data-title="Copy to Clipboard" data-clipboard-target="pull-text">
<i class="fa fa-copy"></i>
</span>
<div class="copy-box" hovering-message="true" value="'docker pull ' + Config.getDomain() + '/' + repo.namespace + '/' + repo.name"></div>
</div>
</div>
<div id="clipboardCopied" class="hovering" style="display: none">
Copied to clipboard
</div>
</span>
</div>
</div>