Merge branch 'master' into git
This commit is contained in:
commit
ba2cb08904
268 changed files with 7008 additions and 1535 deletions
|
@ -35,9 +35,9 @@ quayPages.constant('pages', {
|
|||
}
|
||||
});
|
||||
|
||||
quayDependencies = ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment',
|
||||
'mgcrea.ngStrap', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml',
|
||||
'ngAnimate', 'core-ui', 'core-config-setup', 'quayPages'];
|
||||
quayDependencies = ['ngRoute', 'chieffancypants.loadingBar', 'cfp.hotkeys', 'angular-tour', 'restangular', 'angularMoment',
|
||||
'mgcrea.ngStrap', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml', 'debounce',
|
||||
'core-ui', 'core-config-setup', 'quayPages'];
|
||||
|
||||
if (window.__config && window.__config.MIXPANEL_KEY) {
|
||||
quayDependencies.push('angulartics');
|
||||
|
@ -126,7 +126,10 @@ quayApp.config(['$routeProvider', '$locationProvider', 'pages', function($routeP
|
|||
// Organization View Application
|
||||
.route('/organization/:orgname/application/:clientid', 'manage-application')
|
||||
|
||||
// User Admin
|
||||
// View User
|
||||
.route('/user/:username', 'user-view')
|
||||
|
||||
// DEPRECATED: User Admin
|
||||
.route('/user/', 'user-admin')
|
||||
|
||||
// Sign In
|
||||
|
@ -333,11 +336,14 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
|
|||
});
|
||||
|
||||
var activeTab = $location.search()['tab'];
|
||||
var checkTabs = function() {
|
||||
var tabs = $('a[data-toggle="tab"]');
|
||||
if (tabs.length == 0) {
|
||||
$timeout(checkTabs, 50);
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup deep linking of tabs. This will change the search field of the URL whenever a tab
|
||||
// is changed in the UI.
|
||||
$timeout(function() {
|
||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||
tabs.on('shown.bs.tab', function (e) {
|
||||
var tabName = e.target.getAttribute('data-target').substr(1);
|
||||
$rootScope.$apply(function() {
|
||||
var isDefaultTab = $('a[data-toggle="tab"]')[0] == e.target;
|
||||
|
@ -357,7 +363,11 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
|
|||
if (activeTab) {
|
||||
changeTab(activeTab);
|
||||
}
|
||||
}, 400); // 400ms to make sure angular has rendered.
|
||||
};
|
||||
|
||||
// Setup deep linking of tabs. This will change the search field of the URL whenever a tab
|
||||
// is changed in the UI.
|
||||
$timeout(checkTabs, 50);
|
||||
});
|
||||
|
||||
var initallyChecked = false;
|
||||
|
|
|
@ -12,7 +12,7 @@ angular.module('quay').directive('focusablePopoverContent', ['$timeout', '$popov
|
|||
|
||||
if (!scope) { return; }
|
||||
scope.$apply(function() {
|
||||
if (!scope || !$scope.$hide) { return; }
|
||||
if (!scope || !scope.$hide) { return; }
|
||||
scope.$hide();
|
||||
});
|
||||
};
|
||||
|
|
20
static/js/directives/ng-image-watch.js
Normal file
20
static/js/directives/ng-image-watch.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* Adds a ng-image-watch attribute, which is a callback invoked when the image is loaded or fails.
|
||||
*/
|
||||
angular.module('quay').directive('ngImageWatch', function ($parse) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
compile: function($element, attr) {
|
||||
var fn = $parse(attr['ngImageWatch']);
|
||||
return function(scope, element) {
|
||||
element.bind('error', function() {
|
||||
fn(scope, {result: false});
|
||||
});
|
||||
|
||||
element.bind('load', function() {
|
||||
fn(scope, {result: true});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
|
@ -120,6 +120,8 @@ angular.module('quay').directive('quayClasses', function(Features, Config) {
|
|||
/**
|
||||
* Adds a quay-include attribtue that adds a template solely if the expression evaluates to true.
|
||||
* Automatically adds the Features and Config services to the scope.
|
||||
*
|
||||
Usage: quay-include="{'Features.BILLING': 'partials/landing-normal.html', '!Features.BILLING': 'partials/landing-login.html'}"
|
||||
*/
|
||||
angular.module('quay').directive('quayInclude', function($compile, $templateCache, $http, Features, Config) {
|
||||
return {
|
||||
|
@ -127,7 +129,7 @@ angular.module('quay').directive('quayInclude', function($compile, $templateCach
|
|||
restrict: 'A',
|
||||
link: function($scope, $element, $attr, ctrl) {
|
||||
var getTemplate = function(templateName) {
|
||||
var templateUrl = '/static/partials/' + templateName;
|
||||
var templateUrl = '/static/' + templateName;
|
||||
return $http.get(templateUrl, {cache: $templateCache});
|
||||
};
|
||||
|
||||
|
|
|
@ -12,11 +12,13 @@ angular.module('quay').directive('repoPanelBuilds', function () {
|
|||
'repository': '=repository',
|
||||
'builds': '=builds'
|
||||
},
|
||||
controller: function($scope, $element, $filter, $routeParams, ApiService, TriggerService) {
|
||||
controller: function($scope, $element, $filter, $routeParams, ApiService, TriggerService, UserService) {
|
||||
var orderBy = $filter('orderBy');
|
||||
|
||||
$scope.TriggerService = TriggerService;
|
||||
|
||||
UserService.updateUserIn($scope);
|
||||
|
||||
$scope.options = {
|
||||
'filter': 'recent',
|
||||
'reverse': false,
|
||||
|
@ -66,18 +68,22 @@ angular.module('quay').directive('repoPanelBuilds', function () {
|
|||
if ($scope.buildsResource && filter == $scope.currentFilter) { return; }
|
||||
|
||||
var since = null;
|
||||
var limit = 10;
|
||||
|
||||
if ($scope.options.filter == '48hour') {
|
||||
since = Math.floor(moment().subtract(2, 'days').valueOf() / 1000);
|
||||
limit = 100;
|
||||
} else if ($scope.options.filter == '30day') {
|
||||
since = Math.floor(moment().subtract(30, 'days').valueOf() / 1000);
|
||||
limit = 100;
|
||||
} else {
|
||||
since = null;
|
||||
limit = 10;
|
||||
}
|
||||
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'limit': 100,
|
||||
'limit': limit,
|
||||
'since': since
|
||||
};
|
||||
|
||||
|
@ -175,6 +181,12 @@ angular.module('quay').directive('repoPanelBuilds', function () {
|
|||
};
|
||||
|
||||
$scope.askRunTrigger = function(trigger) {
|
||||
if ($scope.user.username != trigger.connected_user) {
|
||||
bootbox.alert('For security reasons, only user "' + trigger.connected_user +
|
||||
'" can manually invoke this trigger');
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.currentStartTrigger = trigger;
|
||||
$scope.showTriggerStartDialogCounter++;
|
||||
};
|
||||
|
|
|
@ -12,7 +12,16 @@ angular.module('quay').directive('repoPanelInfo', function () {
|
|||
'repository': '=repository',
|
||||
'builds': '=builds'
|
||||
},
|
||||
controller: function($scope, $element, ApiService) {
|
||||
controller: function($scope, $element, ApiService, Config) {
|
||||
$scope.$watch('repository', function(repository) {
|
||||
if (!$scope.repository) { return; }
|
||||
|
||||
var namespace = $scope.repository.namespace;
|
||||
var name = $scope.repository.name;
|
||||
|
||||
$scope.pullCommand = 'docker pull ' + Config.getDomain() + '/' + namespace + '/' + name;
|
||||
});
|
||||
|
||||
$scope.updateDescription = function(content) {
|
||||
$scope.repository.description = content;
|
||||
$scope.repository.put();
|
||||
|
|
|
@ -25,6 +25,7 @@ angular.module('quay').directive('repoPanelTags', function () {
|
|||
|
||||
$scope.iterationState = {};
|
||||
$scope.tagActionHandler = null;
|
||||
$scope.showingHistory = false;
|
||||
|
||||
var setTagState = function() {
|
||||
if (!$scope.repository || !$scope.selectedTags) { return; }
|
||||
|
@ -118,8 +119,142 @@ 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.showingHistory = value;
|
||||
|
||||
if ($scope.showingHistory) {
|
||||
loadTimeline();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.toggleHistory = function() {
|
||||
$scope.showHistory(!$scope.showingHistory);
|
||||
};
|
||||
|
||||
$scope.trackLineClass = function(index, track_info) {
|
||||
var startIndex = $.inArray(track_info.tags[0], $scope.tags);
|
||||
var endIndex = $.inArray(track_info.tags[track_info.tags.length - 1], $scope.tags);
|
||||
|
@ -166,6 +301,10 @@ angular.module('quay').directive('repoPanelTags', function () {
|
|||
$scope.tagActionHandler.askDeleteMultipleTags(tags);
|
||||
};
|
||||
|
||||
$scope.askAddTag = function(tag) {
|
||||
$scope.tagActionHandler.askAddTag(tag.image_id);
|
||||
};
|
||||
|
||||
$scope.orderBy = function(predicate) {
|
||||
if (predicate == $scope.options.predicate) {
|
||||
$scope.options.reverse = !$scope.options.reverse;
|
||||
|
@ -202,6 +341,22 @@ angular.module('quay').directive('repoPanelTags', function () {
|
|||
return $scope.imageIDFilter(it.image_id, tag);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.getTagNames = function(checked) {
|
||||
var names = checked.map(function(tag) {
|
||||
return tag.name;
|
||||
});
|
||||
|
||||
return names.join(',');
|
||||
};
|
||||
|
||||
$scope.isChecked = function(tagName, checked) {
|
||||
return checked.some(function(tag) {
|
||||
if (tag.name == tagName) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
|
|
19
static/js/directives/ui/anchor.js
Normal file
19
static/js/directives/ui/anchor.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* An element which displays its contents wrapped in an <a> tag, but only if the href is not null.
|
||||
*/
|
||||
angular.module('quay').directive('anchor', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/anchor.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'href': '@href',
|
||||
'isOnlyText': '=isOnlyText'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
41
static/js/directives/ui/authorized-apps-manager.js
Normal file
41
static/js/directives/ui/authorized-apps-manager.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* Element for managing the applications authorized by a user.
|
||||
*/
|
||||
angular.module('quay').directive('authorizedAppsManager', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/authorized-apps-manager.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'user': '=user',
|
||||
'isEnabled': '=isEnabled'
|
||||
},
|
||||
controller: function($scope, $element, ApiService) {
|
||||
$scope.$watch('isEnabled', function(enabled) {
|
||||
if (!enabled) { return; }
|
||||
loadAuthedApps();
|
||||
});
|
||||
|
||||
var loadAuthedApps = function() {
|
||||
if ($scope.authorizedAppsResource) { return; }
|
||||
|
||||
$scope.authorizedAppsResource = ApiService.listUserAuthorizationsAsResource().get(function(resp) {
|
||||
$scope.authorizedApps = resp['authorizations'];
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteAccess = function(accessTokenInfo) {
|
||||
var params = {
|
||||
'access_token_uuid': accessTokenInfo['uuid']
|
||||
};
|
||||
|
||||
ApiService.deleteUserAuthorization(null, params).then(function(resp) {
|
||||
$scope.authorizedApps.splice($scope.authorizedApps.indexOf(accessTokenInfo), 1);
|
||||
}, ApiService.errorDisplay('Could not revoke authorization'));
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* An element which displays an avatar for the given {email,name} or hash.
|
||||
* An element which displays an avatar for the given avatar data.
|
||||
*/
|
||||
angular.module('quay').directive('avatar', function () {
|
||||
var directiveDefinitionObject = {
|
||||
|
@ -9,25 +9,38 @@ angular.module('quay').directive('avatar', function () {
|
|||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'hash': '=hash',
|
||||
'email': '=email',
|
||||
'name': '=name',
|
||||
'data': '=data',
|
||||
'size': '=size'
|
||||
},
|
||||
controller: function($scope, $element, AvatarService) {
|
||||
controller: function($scope, $element, AvatarService, Config, UIService, $timeout) {
|
||||
$scope.AvatarService = AvatarService;
|
||||
$scope.Config = Config;
|
||||
$scope.isLoading = true;
|
||||
$scope.hasGravatar = false;
|
||||
$scope.loadGravatar = false;
|
||||
|
||||
var refreshHash = function() {
|
||||
if (!$scope.name && !$scope.email) { return; }
|
||||
$scope._hash = AvatarService.computeHash($scope.email, $scope.name);
|
||||
$scope.imageCallback = function(r) {
|
||||
$timeout(function() {
|
||||
$scope.isLoading = false;
|
||||
$scope.hasGravatar = r;
|
||||
}, 1);
|
||||
};
|
||||
|
||||
$scope.$watch('hash', function(hash) {
|
||||
$scope._hash = hash;
|
||||
$scope.$watch('size', function(size) {
|
||||
size = size * 1 || 16;
|
||||
$scope.fontSize = (size - 4) + 'px';
|
||||
$scope.lineHeight = size + 'px';
|
||||
});
|
||||
|
||||
$scope.$watch('name', refreshHash);
|
||||
$scope.$watch('email', refreshHash);
|
||||
$scope.$watch('data', function(data) {
|
||||
if (!data) { return; }
|
||||
|
||||
$scope.loadGravatar = Config.AVATAR_KIND == 'gravatar' &&
|
||||
(data.kind == 'user' || data.kind == 'org');
|
||||
|
||||
$scope.isLoading = $scope.loadGravatar;
|
||||
$scope.hasGravatar = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
|
|
|
@ -15,11 +15,6 @@ angular.module('quay').directive('billingInvoices', function () {
|
|||
},
|
||||
controller: function($scope, $element, $sce, ApiService) {
|
||||
$scope.loading = false;
|
||||
$scope.invoiceExpanded = {};
|
||||
|
||||
$scope.toggleInvoice = function(id) {
|
||||
$scope.invoiceExpanded[id] = !$scope.invoiceExpanded[id];
|
||||
};
|
||||
|
||||
var update = function() {
|
||||
var hasValidUser = !!$scope.user;
|
||||
|
@ -35,6 +30,9 @@ angular.module('quay').directive('billingInvoices', function () {
|
|||
ApiService.listInvoices($scope.organization).then(function(resp) {
|
||||
$scope.invoices = resp.invoices;
|
||||
$scope.loading = false;
|
||||
}, function() {
|
||||
$scope.invoices = [];
|
||||
$scope.loading = false;
|
||||
});
|
||||
};
|
||||
|
|
@ -9,7 +9,8 @@ angular.module('quay').directive('buildMiniStatus', function () {
|
|||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'build': '=build'
|
||||
'build': '=build',
|
||||
'isAdmin': '=isAdmin'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.isBuilding = function(build) {
|
||||
|
|
62
static/js/directives/ui/convert-user-to-org.js
Normal file
62
static/js/directives/ui/convert-user-to-org.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* Displays a panel for converting the current user to an organization.
|
||||
*/
|
||||
angular.module('quay').directive('convertUserToOrg', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/convert-user-to-org.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'user': '=user'
|
||||
},
|
||||
controller: function($scope, $element, Features, PlanService, Config) {
|
||||
$scope.convertStep = 0;
|
||||
|
||||
$scope.showConvertForm = function() {
|
||||
if (Features.BILLING) {
|
||||
PlanService.getMatchingBusinessPlan(function(plan) {
|
||||
$scope.org.plan = plan;
|
||||
});
|
||||
|
||||
PlanService.getPlans(function(plans) {
|
||||
$scope.orgPlans = plans;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.convertStep = 1;
|
||||
};
|
||||
|
||||
$scope.convertToOrg = function() {
|
||||
$('#reallyconvertModal').modal({});
|
||||
};
|
||||
|
||||
$scope.reallyConvert = function() {
|
||||
if (Config.AUTHENTICATION_TYPE != 'Database') { return; }
|
||||
|
||||
$scope.loading = true;
|
||||
|
||||
var data = {
|
||||
'adminUser': $scope.org.adminUser,
|
||||
'adminPassword': $scope.org.adminPassword,
|
||||
'plan': $scope.org.plan ? $scope.org.plan.stripeId : ''
|
||||
};
|
||||
|
||||
ApiService.convertUserToOrganization(data).then(function(resp) {
|
||||
CookieService.putPermanent('quay.namespace', $scope.cuser.username);
|
||||
UserService.load();
|
||||
$location.path('/');
|
||||
}, function(resp) {
|
||||
$scope.loading = false;
|
||||
if (resp.data.reason == 'invaliduser') {
|
||||
$('#invalidadminModal').modal({});
|
||||
} else {
|
||||
$('#cannotconvertModal').modal({});
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -39,6 +39,21 @@ angular.module('quay').directive('entityReference', function () {
|
|||
return '/organization/' + org['name'] + '/admin?tab=robots&showRobot=' + UtilService.textToSafeHtml(name);
|
||||
};
|
||||
|
||||
$scope.getTitle = function(entity) {
|
||||
if (!entity) { return ''; }
|
||||
|
||||
switch (entity.kind) {
|
||||
case 'org':
|
||||
return 'Organization';
|
||||
|
||||
case 'team':
|
||||
return 'Team';
|
||||
|
||||
case 'user':
|
||||
return entity.is_robot ? 'Robot Account' : 'User';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getPrefix = function(name) {
|
||||
if (!name) { return ''; }
|
||||
var plus = name.indexOf('+');
|
||||
|
|
|
@ -56,6 +56,8 @@ angular.module('quay').directive('entitySearch', function () {
|
|||
|
||||
$scope.currentEntityInternal = $scope.currentEntity;
|
||||
|
||||
$scope.Config = Config;
|
||||
|
||||
var isSupported = function(kind, opt_array) {
|
||||
return $.inArray(kind, opt_array || $scope.allowedEntities || ['user', 'team', 'robot']) >= 0;
|
||||
};
|
||||
|
@ -90,48 +92,25 @@ angular.module('quay').directive('entitySearch', function () {
|
|||
};
|
||||
|
||||
$scope.createTeam = function() {
|
||||
if (!$scope.isAdmin) { return; }
|
||||
|
||||
bootbox.prompt('Enter the name of the new team', function(teamname) {
|
||||
if (!teamname) { return; }
|
||||
|
||||
var regex = new RegExp(TEAM_PATTERN);
|
||||
if (!regex.test(teamname)) {
|
||||
bootbox.alert('Invalid team name');
|
||||
return;
|
||||
}
|
||||
|
||||
CreateService.createOrganizationTeam(ApiService, $scope.namespace, teamname, function(created) {
|
||||
$scope.setEntity(created.name, 'team', false);
|
||||
$scope.teams[teamname] = created;
|
||||
});
|
||||
CreateService.askCreateTeam($scope.namespace, function(created) {
|
||||
$scope.setEntity(created.name, 'team', false, created.avatar);
|
||||
$scope.teams[teamname] = created;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.createRobot = function() {
|
||||
if (!$scope.isAdmin) { return; }
|
||||
|
||||
bootbox.prompt('Enter the name of the new robot account', function(robotname) {
|
||||
if (!robotname) { return; }
|
||||
|
||||
var regex = new RegExp(ROBOT_PATTERN);
|
||||
if (!regex.test(robotname)) {
|
||||
bootbox.alert('Invalid robot account name');
|
||||
return;
|
||||
}
|
||||
|
||||
CreateService.createRobotAccount(ApiService, $scope.isOrganization, $scope.namespace, robotname, function(created) {
|
||||
$scope.setEntity(created.name, 'user', true);
|
||||
$scope.robots.push(created);
|
||||
});
|
||||
CreateService.askCreateRobot($scope.namespace, function(created) {
|
||||
$scope.setEntity(created.name, 'user', true, created.avatar);
|
||||
$scope.robots.push(created);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.setEntity = function(name, kind, is_robot) {
|
||||
$scope.setEntity = function(name, kind, is_robot, avatar) {
|
||||
var entity = {
|
||||
'name': name,
|
||||
'kind': kind,
|
||||
'is_robot': is_robot
|
||||
'is_robot': is_robot,
|
||||
'avatar': avatar
|
||||
};
|
||||
|
||||
if ($scope.isOrganization) {
|
||||
|
|
|
@ -11,6 +11,7 @@ angular.module('quay').directive('externalLoginButton', function () {
|
|||
scope: {
|
||||
'signInStarted': '&signInStarted',
|
||||
'redirectUrl': '=redirectUrl',
|
||||
'isLink': '=isLink',
|
||||
'provider': '@provider',
|
||||
'action': '@action'
|
||||
},
|
||||
|
|
55
static/js/directives/ui/external-logins-manager.js
Normal file
55
static/js/directives/ui/external-logins-manager.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* Element for managing the applications authorized by a user.
|
||||
*/
|
||||
angular.module('quay').directive('externalLoginsManager', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/external-logins-manager.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'user': '=user',
|
||||
},
|
||||
controller: function($scope, $element, ApiService, UserService, Features, Config, KeyService) {
|
||||
$scope.Features = Features;
|
||||
$scope.Config = Config;
|
||||
$scope.KeyService = KeyService;
|
||||
|
||||
UserService.updateUserIn($scope, function(user) {
|
||||
$scope.cuser = jQuery.extend({}, user);
|
||||
|
||||
if ($scope.cuser.logins) {
|
||||
for (var i = 0; i < $scope.cuser.logins.length; i++) {
|
||||
var login = $scope.cuser.logins[i];
|
||||
login.metadata = login.metadata || {};
|
||||
|
||||
if (login.service == 'github') {
|
||||
$scope.hasGithubLogin = true;
|
||||
$scope.githubLogin = login.metadata['service_username'];
|
||||
$scope.githubEndpoint = KeyService['githubEndpoint'];
|
||||
}
|
||||
|
||||
if (login.service == 'google') {
|
||||
$scope.hasGoogleLogin = true;
|
||||
$scope.googleLogin = login.metadata['service_username'];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$scope.detachExternalLogin = function(kind) {
|
||||
var params = {
|
||||
'servicename': kind
|
||||
};
|
||||
|
||||
ApiService.detachExternalLogin(null, params).then(function() {
|
||||
$scope.hasGithubLogin = false;
|
||||
$scope.hasGoogleLogin = false;
|
||||
UserService.load();
|
||||
}, ApiService.errorDisplay('Count not detach service'));
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
114
static/js/directives/ui/fetch-tag-dialog.js
Normal file
114
static/js/directives/ui/fetch-tag-dialog.js
Normal file
|
@ -0,0 +1,114 @@
|
|||
/**
|
||||
* An element which adds a of dialog for fetching a tag.
|
||||
*/
|
||||
angular.module('quay').directive('fetchTagDialog', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/fetch-tag-dialog.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'actionHandler': '=actionHandler'
|
||||
},
|
||||
controller: function($scope, $element, $timeout, ApiService, UserService, Config) {
|
||||
$scope.clearCounter = 0;
|
||||
$scope.currentFormat = null;
|
||||
$scope.currentEntity = null;
|
||||
$scope.currentRobot = null;
|
||||
$scope.formats = [];
|
||||
|
||||
UserService.updateUserIn($scope, updateFormats);
|
||||
|
||||
var updateFormats = function() {
|
||||
$scope.formats = [];
|
||||
|
||||
if ($scope.repository && UserService.isNamespaceAdmin($scope.repository.namespace)) {
|
||||
$scope.formats.push({
|
||||
'title': 'Squashed Docker Image',
|
||||
'icon': 'ci-squashed',
|
||||
'command': 'curl -L -f {http}://{pull_user}:{pull_password}@{hostname}/c1/squash/{namespace}/{name}/{tag} | docker load',
|
||||
'require_creds': true
|
||||
});
|
||||
}
|
||||
|
||||
$scope.formats.push({
|
||||
'title': 'Basic Docker Pull',
|
||||
'icon': 'docker-icon',
|
||||
'command': 'docker pull {hostname}/{namespace}/{name}:{tag}'
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('currentEntity', function(entity) {
|
||||
if (!entity) {
|
||||
$scope.currentRobot = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if ($scope.currentRobot && $scope.currentRobot.name == entity.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.currentRobot = null;
|
||||
|
||||
var parts = entity.name.split('+');
|
||||
var namespace = parts[0];
|
||||
var shortname = parts[1];
|
||||
|
||||
var params = {
|
||||
'robot_shortname': shortname
|
||||
};
|
||||
|
||||
var orgname = UserService.isOrganization(namespace) ? namespace : '';
|
||||
ApiService.getRobot(orgname, null, params).then(function(resp) {
|
||||
$scope.currentRobot = resp;
|
||||
}, ApiService.errorDisplay('Cannot download robot token'));
|
||||
});
|
||||
|
||||
$scope.getCommand = function(format, robot) {
|
||||
if (!format || !format.command) { return ''; }
|
||||
if (format.require_creds && !robot) { return ''; }
|
||||
|
||||
var params = {
|
||||
'pull_user': robot ? robot.name : '',
|
||||
'pull_password': robot ? robot.token : '',
|
||||
'hostname': Config.getDomain(),
|
||||
'http': Config.getHttp(),
|
||||
'namespace': $scope.repository.namespace,
|
||||
'name': $scope.repository.name,
|
||||
'tag': $scope.currentTag.name
|
||||
};
|
||||
|
||||
var value = format.command;
|
||||
for (var param in params) {
|
||||
if (!params.hasOwnProperty(param)) { continue; }
|
||||
value = value.replace('{' + param + '}', params[param]);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
$scope.setFormat = function(format) {
|
||||
$scope.currentFormat = format;
|
||||
};
|
||||
|
||||
$scope.actionHandler = {
|
||||
'askFetchTag': function(tag) {
|
||||
$scope.currentTag = tag;
|
||||
$scope.currentFormat = null;
|
||||
$scope.currentEntity = null;
|
||||
$scope.currentRobot = null;
|
||||
|
||||
$scope.clearCounter++;
|
||||
|
||||
updateFormats();
|
||||
|
||||
$element.find('#copyClipboard').clipboardCopy();
|
||||
$element.find('#fetchTagDialog').modal({});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -12,12 +12,81 @@ angular.module('quay').directive('headerBar', function () {
|
|||
restrict: 'C',
|
||||
scope: {
|
||||
},
|
||||
controller: function($scope, $element, $location, UserService, PlanService, ApiService, NotificationService, Config) {
|
||||
controller: function($rootScope, $scope, $element, $location, $timeout, hotkeys, UserService, PlanService, ApiService, NotificationService, Config, CreateService) {
|
||||
$scope.isNewLayout = Config.isNewLayout();
|
||||
|
||||
if ($scope.isNewLayout) {
|
||||
// Register hotkeys:
|
||||
hotkeys.add({
|
||||
combo: '/',
|
||||
description: 'Show search',
|
||||
callback: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$scope.toggleSearch();
|
||||
}
|
||||
});
|
||||
|
||||
hotkeys.add({
|
||||
combo: 'alt+c',
|
||||
description: 'Create new repository',
|
||||
callback: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$location.url('/new');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$scope.notificationService = NotificationService;
|
||||
$scope.searchVisible = false;
|
||||
$scope.currentSearchQuery = null;
|
||||
$scope.searchResultState = null;
|
||||
$scope.showBuildDialogCounter = 0;
|
||||
|
||||
// Monitor any user changes and place the current user into the scope.
|
||||
UserService.updateUserIn($scope);
|
||||
|
||||
$scope.currentPageContext = {};
|
||||
|
||||
$rootScope.$watch('currentPage.scope.viewuser', function(u) {
|
||||
$scope.currentPageContext['viewuser'] = u;
|
||||
});
|
||||
|
||||
$rootScope.$watch('currentPage.scope.organization', function(o) {
|
||||
$scope.currentPageContext['organization'] = o;
|
||||
});
|
||||
|
||||
$rootScope.$watch('currentPage.scope.repository', function(r) {
|
||||
$scope.currentPageContext['repository'] = r;
|
||||
});
|
||||
|
||||
var conductSearch = function(query) {
|
||||
if (!query) { $scope.searchResultState = null; return; }
|
||||
|
||||
$scope.searchResultState = {
|
||||
'state': 'loading'
|
||||
};
|
||||
|
||||
var params = {
|
||||
'query': query
|
||||
};
|
||||
|
||||
ApiService.conductSearch(null, params).then(function(resp) {
|
||||
if (!$scope.searchVisible || query != $scope.currentSearchQuery) { return; }
|
||||
|
||||
$scope.searchResultState = {
|
||||
'state': resp.results.length ? 'results' : 'no-results',
|
||||
'results': resp.results,
|
||||
'current': resp.results.length ? 0 : -1
|
||||
};
|
||||
}, function(resp) {
|
||||
$scope.searchResultState = null;
|
||||
}, /* background */ true);
|
||||
};
|
||||
|
||||
$scope.$watch('currentSearchQuery', conductSearch);
|
||||
|
||||
$scope.signout = function() {
|
||||
ApiService.logout().then(function() {
|
||||
UserService.load();
|
||||
|
@ -39,6 +108,126 @@ angular.module('quay').directive('headerBar', function () {
|
|||
|
||||
return Config.ENTERPRISE_LOGO_URL;
|
||||
};
|
||||
|
||||
$scope.toggleSearch = function() {
|
||||
$scope.searchVisible = !$scope.searchVisible;
|
||||
if ($scope.searchVisible) {
|
||||
$('#search-box-input').focus();
|
||||
if ($scope.currentSearchQuery) {
|
||||
conductSearch($scope.currentSearchQuery);
|
||||
}
|
||||
} else {
|
||||
$('#search-box-input').blur()
|
||||
$scope.searchResultState = null;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getSearchBoxClasses = function(searchVisible, searchResultState) {
|
||||
var classes = searchVisible ? 'search-visible ' : '';
|
||||
if (searchResultState) {
|
||||
classes += 'results-visible';
|
||||
}
|
||||
return classes;
|
||||
};
|
||||
|
||||
$scope.handleSearchKeyDown = function(e) {
|
||||
if (e.keyCode == 27) {
|
||||
$scope.toggleSearch();
|
||||
return;
|
||||
}
|
||||
|
||||
var state = $scope.searchResultState;
|
||||
if (!state || !state['results']) { return; }
|
||||
|
||||
if (e.keyCode == 40) {
|
||||
state['current']++;
|
||||
e.preventDefault();
|
||||
} else if (e.keyCode == 38) {
|
||||
state['current']--;
|
||||
e.preventDefault();
|
||||
} else if (e.keyCode == 13) {
|
||||
var current = state['current'];
|
||||
if (current >= 0 && current < state['results'].length) {
|
||||
$scope.showResult(state['results'][current]);
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (state['current'] < -1) {
|
||||
state['current'] = state['results'].length - 1;
|
||||
} else if (state['current'] >= state['results'].length) {
|
||||
state['current'] = 0;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.showResult = function(result) {
|
||||
$scope.toggleSearch();
|
||||
$timeout(function() {
|
||||
$scope.currentSearchQuery = '';
|
||||
$location.url(result['href'])
|
||||
}, 500);
|
||||
};
|
||||
|
||||
$scope.setCurrentResult = function(result) {
|
||||
if (!$scope.searchResultState) { return; }
|
||||
$scope.searchResultState['current'] = result;
|
||||
};
|
||||
|
||||
$scope.getNamespace = function(context) {
|
||||
if (!context) { return null; }
|
||||
|
||||
if (context.repository && context.repository.namespace) {
|
||||
return context.repository.namespace;
|
||||
}
|
||||
|
||||
if (context.organization && context.organization.name) {
|
||||
return context.organization.name;
|
||||
}
|
||||
|
||||
if (context.viewuser && context.viewuser.username) {
|
||||
return context.viewuser.username;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
$scope.canAdmin = function(namespace) {
|
||||
if (!namespace) { return false; }
|
||||
return UserService.isNamespaceAdmin(namespace);
|
||||
};
|
||||
|
||||
$scope.isOrganization = function(namespace) {
|
||||
if (!namespace) { return false; }
|
||||
return UserService.isOrganization(namespace);
|
||||
};
|
||||
|
||||
$scope.startBuild = function(context) {
|
||||
$scope.showBuildDialogCounter++;
|
||||
};
|
||||
|
||||
$scope.handleBuildStarted = function(build, context) {
|
||||
$location.url('/repository/' + context.repository.namespace + '/' + context.repository.name + '/build/' + build.id);
|
||||
};
|
||||
|
||||
$scope.createRobot = function(context) {
|
||||
var namespace = $scope.getNamespace(context);
|
||||
CreateService.askCreateRobot(function(created) {
|
||||
if (isorg) {
|
||||
$location.url('/organization/' + namespace + '?tab=robots');
|
||||
} else {
|
||||
$location.url('/user/' + namespace + '?tab=robots');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.createTeam = function(context) {
|
||||
var namespace = $scope.getNamespace(context);
|
||||
if (!namespace || !UserService.isNamespaceAdmin(namespace)) { return; }
|
||||
|
||||
CreateService.askCreateTeam(function(created) {
|
||||
$location.url('/organization/' + namespace + '/teams/' + teamname);
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
|
|
19
static/js/directives/ui/image-link.js
Normal file
19
static/js/directives/ui/image-link.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* An element which displays a link to a repository image.
|
||||
*/
|
||||
angular.module('quay').directive('imageLink', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/image-link.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'imageId': '=imageId'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
49
static/js/directives/ui/image-view-layer.js
Normal file
49
static/js/directives/ui/image-view-layer.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* An element which displays a single layer representing an image in the image view.
|
||||
*/
|
||||
angular.module('quay').directive('imageViewLayer', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/image-view-layer.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'image': '=image',
|
||||
'images': '=images'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.getDockerfileCommand = function(command) {
|
||||
if (!command) { return ''; }
|
||||
|
||||
// ["/bin/sh", "-c", "#(nop) RUN foo"]
|
||||
var commandPrefix = '#(nop)'
|
||||
|
||||
if (command.length != 3) { return ''; }
|
||||
if (command[0] != '/bin/sh' || command[1] != '-c') { return ''; }
|
||||
|
||||
var cmd = command[2];
|
||||
if (cmd.substring(0, commandPrefix.length) != commandPrefix) {
|
||||
return 'RUN ' + cmd;
|
||||
}
|
||||
|
||||
return command[2].substr(commandPrefix.length + 1);
|
||||
};
|
||||
|
||||
$scope.getClass = function() {
|
||||
var index = $.inArray($scope.image, $scope.images);
|
||||
if (index < 0) {
|
||||
return 'first';
|
||||
}
|
||||
|
||||
if (index == $scope.images.length - 1) {
|
||||
return 'last';
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -11,9 +11,9 @@ angular.module('quay').directive('repoListGrid', function () {
|
|||
scope: {
|
||||
repositoriesResource: '=repositoriesResource',
|
||||
starred: '=starred',
|
||||
user: "=user",
|
||||
namespace: '=namespace',
|
||||
starToggled: '&starToggled'
|
||||
starToggled: '&starToggled',
|
||||
hideTitle: '=hideTitle'
|
||||
},
|
||||
controller: function($scope, $element, UserService) {
|
||||
$scope.isOrganization = function(namespace) {
|
||||
|
|
|
@ -2,6 +2,21 @@
|
|||
* An element which displays a table of permissions on a repository and allows them to be
|
||||
* edited.
|
||||
*/
|
||||
angular.module('quay').filter('objectFilter', function() {
|
||||
return function(obj, filterFn) {
|
||||
if (!obj) { return []; }
|
||||
|
||||
var result = [];
|
||||
angular.forEach(obj, function(value) {
|
||||
if (filterFn(value)) {
|
||||
result.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
});
|
||||
|
||||
angular.module('quay').directive('repositoryPermissionsTable', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
|
@ -13,6 +28,7 @@ angular.module('quay').directive('repositoryPermissionsTable', function () {
|
|||
'repository': '=repository'
|
||||
},
|
||||
controller: function($scope, $element, ApiService, Restangular, UtilService) {
|
||||
// TODO(jschorr): move this to a service.
|
||||
$scope.roles = [
|
||||
{ 'id': 'read', 'title': 'Read', 'kind': 'success' },
|
||||
{ 'id': 'write', 'title': 'Write', 'kind': 'success' },
|
||||
|
@ -58,21 +74,50 @@ angular.module('quay').directive('repositoryPermissionsTable', function () {
|
|||
return Restangular.one(url);
|
||||
};
|
||||
|
||||
$scope.buildEntityForPermission = function(name, permission, kind) {
|
||||
var key = name + ':' + kind;
|
||||
$scope.buildEntityForPermission = function(permission, kind) {
|
||||
var key = permission.name + ':' + kind;
|
||||
if ($scope.permissionCache[key]) {
|
||||
return $scope.permissionCache[key];
|
||||
}
|
||||
|
||||
return $scope.permissionCache[key] = {
|
||||
'kind': kind,
|
||||
'name': name,
|
||||
'name': permission.name,
|
||||
'is_robot': permission.is_robot,
|
||||
'is_org_member': permission.is_org_member
|
||||
'is_org_member': permission.is_org_member,
|
||||
'avatar': permission.avatar
|
||||
};
|
||||
};
|
||||
|
||||
$scope.addPermission = function() {
|
||||
$scope.hasPermissions = function(teams, users) {
|
||||
if (teams && teams.value) {
|
||||
if (Object.keys(teams.value).length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (users && users.value) {
|
||||
if (Object.keys(users.value).length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
$scope.allEntries = function() {
|
||||
return true;
|
||||
};
|
||||
|
||||
$scope.onlyRobot = function(permission) {
|
||||
return permission.is_robot == true;
|
||||
};
|
||||
|
||||
$scope.onlyUser = function(permission) {
|
||||
return !permission.is_robot;
|
||||
};
|
||||
|
||||
$scope.addPermission = function() {
|
||||
$scope.addPermissionInfo['working'] = true;
|
||||
$scope.addNewPermission($scope.addPermissionInfo.entity, $scope.addPermissionInfo.role)
|
||||
};
|
||||
|
|
|
@ -12,12 +12,38 @@ angular.module('quay').directive('robotsManager', function () {
|
|||
'organization': '=organization',
|
||||
'user': '=user'
|
||||
},
|
||||
controller: function($scope, $element, ApiService, $routeParams, CreateService) {
|
||||
controller: function($scope, $element, ApiService, $routeParams, CreateService, Config) {
|
||||
$scope.ROBOT_PATTERN = ROBOT_PATTERN;
|
||||
|
||||
// TODO(jschorr): move this to a service.
|
||||
$scope.roles = [
|
||||
{ 'id': 'read', 'title': 'Read', 'kind': 'success' },
|
||||
{ 'id': 'write', 'title': 'Write', 'kind': 'success' },
|
||||
{ 'id': 'admin', 'title': 'Admin', 'kind': 'primary' }
|
||||
];
|
||||
|
||||
$scope.robots = null;
|
||||
$scope.loading = false;
|
||||
$scope.shownRobot = null;
|
||||
$scope.showRobotCounter = 0;
|
||||
$scope.Config = Config;
|
||||
|
||||
var loadRobotPermissions = function(info) {
|
||||
var shortName = $scope.getShortenedName(info.name);
|
||||
info.loading_permissions = true;
|
||||
ApiService.getRobotPermissions($scope.organization, null, {'robot_shortname': shortName}).then(function(resp) {
|
||||
info.permissions = resp.permissions;
|
||||
info.loading_permissions = false;
|
||||
}, ApiService.errorDisplay('Could not load robot permissions'));
|
||||
};
|
||||
|
||||
$scope.showPermissions = function(robotInfo) {
|
||||
robotInfo.showing_permissions = !robotInfo.showing_permissions;
|
||||
|
||||
if (robotInfo.showing_permissions) {
|
||||
loadRobotPermissions(robotInfo);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.regenerateToken = function(username) {
|
||||
if (!username) { return; }
|
||||
|
@ -47,6 +73,10 @@ angular.module('quay').directive('robotsManager', function () {
|
|||
return -1;
|
||||
};
|
||||
|
||||
$scope.getShortenedRobotName = function(info) {
|
||||
return $scope.getShortenedName(info.name);
|
||||
};
|
||||
|
||||
$scope.getShortenedName = function(name) {
|
||||
var plus = name.indexOf('+');
|
||||
return name.substr(plus + 1);
|
||||
|
|
|
@ -12,6 +12,7 @@ angular.module('quay').directive('roleGroup', function () {
|
|||
scope: {
|
||||
'roles': '=roles',
|
||||
'currentRole': '=currentRole',
|
||||
'readOnly': '=readOnly',
|
||||
'roleChanged': '&roleChanged'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
|
|
131
static/js/directives/ui/teams-manager.js
Normal file
131
static/js/directives/ui/teams-manager.js
Normal file
|
@ -0,0 +1,131 @@
|
|||
/**
|
||||
* Element for managing the teams of an organization.
|
||||
*/
|
||||
angular.module('quay').directive('teamsManager', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/teams-manager.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'organization': '=organization'
|
||||
},
|
||||
controller: function($scope, $element, ApiService, CreateService) {
|
||||
$scope.TEAM_PATTERN = TEAM_PATTERN;
|
||||
$scope.teamRoles = [
|
||||
{ 'id': 'member', 'title': 'Member', 'kind': 'default' },
|
||||
{ 'id': 'creator', 'title': 'Creator', 'kind': 'success' },
|
||||
{ 'id': 'admin', 'title': 'Admin', 'kind': 'primary' }
|
||||
];
|
||||
|
||||
$scope.members = {};
|
||||
$scope.orderedTeams = [];
|
||||
|
||||
var loadTeamMembers = function() {
|
||||
if (!$scope.organization) { return; }
|
||||
|
||||
for (var name in $scope.organization.teams) {
|
||||
if (!$scope.organization.teams.hasOwnProperty(name)) { continue; }
|
||||
loadMembersOfTeam(name);
|
||||
}
|
||||
};
|
||||
|
||||
var loadMembersOfTeam = function(name) {
|
||||
var params = {
|
||||
'orgname': $scope.organization.name,
|
||||
'teamname': name
|
||||
};
|
||||
|
||||
$scope.members[name] = {};
|
||||
|
||||
ApiService.getOrganizationTeamMembers(null, params).then(function(resp) {
|
||||
$scope.members[name].members = resp.members;
|
||||
}, function() {
|
||||
delete $scope.members[name];
|
||||
});
|
||||
};
|
||||
|
||||
var loadOrderedTeams = function() {
|
||||
if (!$scope.organization || !$scope.organization.ordered_teams) { return; }
|
||||
|
||||
$scope.orderedTeams = [];
|
||||
$scope.organization.ordered_teams.map(function(name) {
|
||||
$scope.orderedTeams.push($scope.organization.teams[name]);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('organization', loadOrderedTeams);
|
||||
$scope.$watch('organization', loadTeamMembers);
|
||||
|
||||
$scope.setRole = function(role, teamname) {
|
||||
var previousRole = $scope.organization.teams[teamname].role;
|
||||
$scope.organization.teams[teamname].role = role;
|
||||
|
||||
var params = {
|
||||
'orgname': $scope.organization.name,
|
||||
'teamname': teamname
|
||||
};
|
||||
|
||||
var data = $scope.organization.teams[teamname];
|
||||
|
||||
var errorHandler = ApiService.errorDisplay('Cannot update team', function(resp) {
|
||||
$scope.organization.teams[teamname].role = previousRole;
|
||||
});
|
||||
|
||||
ApiService.updateOrganizationTeam(data, params).then(function(resp) {
|
||||
}, errorHandler);
|
||||
};
|
||||
|
||||
$scope.createTeam = function(teamname) {
|
||||
if (!teamname) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($scope.organization.teams[teamname]) {
|
||||
$('#team-' + teamname).removeClass('highlight');
|
||||
setTimeout(function() {
|
||||
$('#team-' + teamname).addClass('highlight');
|
||||
}, 10);
|
||||
return;
|
||||
}
|
||||
|
||||
var orgname = $scope.organization.name;
|
||||
CreateService.createOrganizationTeam(ApiService, orgname, teamname, function(created) {
|
||||
$scope.organization.teams[teamname] = created;
|
||||
$scope.members[teamname] = {};
|
||||
$scope.members[teamname].members = [];
|
||||
$scope.organization.ordered_teams.push(teamname);
|
||||
$scope.orderedTeams.push(created);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.askDeleteTeam = function(teamname) {
|
||||
bootbox.confirm('Are you sure you want to delete team ' + teamname + '?', function(resp) {
|
||||
if (resp) {
|
||||
$scope.deleteTeam(teamname);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteTeam = function(teamname) {
|
||||
var params = {
|
||||
'orgname': $scope.organization.name,
|
||||
'teamname': teamname
|
||||
};
|
||||
|
||||
ApiService.deleteOrganizationTeam(null, params).then(function() {
|
||||
var index = $scope.organization.ordered_teams.indexOf(teamname);
|
||||
if (index >= 0) {
|
||||
$scope.organization.ordered_teams.splice(index, 1);
|
||||
}
|
||||
|
||||
loadOrderedTeams();
|
||||
delete $scope.organization.teams[teamname];
|
||||
}, ApiService.errorDisplay('Cannot delete team'));
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -95,7 +95,7 @@ function ImageHistoryTree(namespace, name, images, formatComment, formatTime, fo
|
|||
* Calculates the dimensions of the tree.
|
||||
*/
|
||||
ImageHistoryTree.prototype.calculateDimensions_ = function(container) {
|
||||
var cw = Math.max(document.getElementById(container).clientWidth, this.maxWidth_ * DEPTH_WIDTH);
|
||||
var cw = document.getElementById(container).clientWidth;
|
||||
var ch = this.maxHeight_ * (DEPTH_HEIGHT + 10);
|
||||
|
||||
var margin = { top: 40, right: 20, bottom: 20, left: 80 };
|
||||
|
@ -1157,8 +1157,11 @@ FileTreeBase.prototype.populateAndDraw_ = function() {
|
|||
}
|
||||
|
||||
this.root_ = this.nodeMap_[''];
|
||||
this.root_.x0 = 0;
|
||||
this.root_.y0 = 0;
|
||||
if (this.root_) {
|
||||
this.root_.x0 = 0;
|
||||
this.root_.y0 = 0;
|
||||
}
|
||||
|
||||
this.toggle_(this.root_);
|
||||
this.update_(this.root_);
|
||||
};
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
$scope.setEnabled = function(value) {
|
||||
$scope.isEnabled = value;
|
||||
CookieService.putPermanent('quay.exp-new-layout', value.toString());
|
||||
document.location.reload();
|
||||
};
|
||||
}
|
||||
}());
|
|
@ -3,7 +3,14 @@
|
|||
* Page to view the details of a single image.
|
||||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('image-view', 'image-view.html', ImageViewCtrl);
|
||||
pages.create('image-view', 'image-view.html', ImageViewCtrl, {
|
||||
'newLayout': true,
|
||||
'title': '{{ image.id }}',
|
||||
'description': 'Image {{ image.id }}'
|
||||
}, ['layout'])
|
||||
|
||||
pages.create('image-view', 'old-image-view.html', OldImageViewCtrl, {
|
||||
}, ['old-layout']);
|
||||
}]);
|
||||
|
||||
function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, ImageMetadataService) {
|
||||
|
@ -11,6 +18,75 @@
|
|||
var name = $routeParams.name;
|
||||
var imageid = $routeParams.image;
|
||||
|
||||
var loadImage = function() {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name,
|
||||
'image_id': imageid
|
||||
};
|
||||
|
||||
$scope.imageResource = ApiService.getImageAsResource(params).get(function(image) {
|
||||
$scope.image = image;
|
||||
$scope.reversedHistory = image.history.reverse();
|
||||
});
|
||||
};
|
||||
|
||||
var loadRepository = function() {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name
|
||||
};
|
||||
|
||||
$scope.repositoryResource = ApiService.getRepoAsResource(params).get(function(repo) {
|
||||
$scope.repository = repo;
|
||||
});
|
||||
};
|
||||
|
||||
loadImage();
|
||||
loadRepository();
|
||||
|
||||
$scope.downloadChanges = function() {
|
||||
if ($scope.changesResource) { return; }
|
||||
|
||||
var params = {
|
||||
'repository': namespace + '/' + name,
|
||||
'image_id': imageid
|
||||
};
|
||||
|
||||
$scope.changesResource = ApiService.getImageChangesAsResource(params).get(function(changes) {
|
||||
var combinedChanges = [];
|
||||
var addCombinedChanges = function(c, kind) {
|
||||
for (var i = 0; i < c.length; ++i) {
|
||||
combinedChanges.push({
|
||||
'kind': kind,
|
||||
'file': c[i]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
addCombinedChanges(changes.added, 'added');
|
||||
addCombinedChanges(changes.removed, 'removed');
|
||||
addCombinedChanges(changes.changed, 'changed');
|
||||
|
||||
$scope.combinedChanges = combinedChanges;
|
||||
$scope.imageChanges = changes;
|
||||
$scope.initializeTree();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.initializeTree = function() {
|
||||
if ($scope.tree || !$scope.combinedChanges.length) { return; }
|
||||
|
||||
$scope.tree = new ImageFileChangeTree($scope.image, $scope.combinedChanges);
|
||||
$timeout(function() {
|
||||
$scope.tree.draw('changes-tree-container');
|
||||
}, 100);
|
||||
};
|
||||
}
|
||||
|
||||
function OldImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, ImageMetadataService) {
|
||||
var namespace = $routeParams.namespace;
|
||||
var name = $routeParams.name;
|
||||
var imageid = $routeParams.image;
|
||||
|
||||
$scope.getFormattedCommand = ImageMetadataService.getFormattedCommand;
|
||||
|
||||
$scope.parseDate = function(dateString) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
(function() {
|
||||
/**
|
||||
* Landing page.
|
||||
* DEPRECATED: Remove the code for viewing when logged in.
|
||||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('landing', 'landing.html', LandingCtrl, {
|
||||
|
@ -8,7 +9,7 @@
|
|||
});
|
||||
}]);
|
||||
|
||||
function LandingCtrl($scope, UserService, ApiService, Features, Config) {
|
||||
function LandingCtrl($scope, $location, UserService, ApiService, Features, Config) {
|
||||
$scope.namespace = null;
|
||||
$scope.currentScreenshot = 'repo-view';
|
||||
|
||||
|
@ -16,7 +17,12 @@
|
|||
loadMyRepos(namespace);
|
||||
});
|
||||
|
||||
UserService.updateUserIn($scope, function() {
|
||||
UserService.updateUserIn($scope, function(user) {
|
||||
if (!user.anonymous && Config.isNewLayout()) {
|
||||
$location.path('/repository');
|
||||
return;
|
||||
}
|
||||
|
||||
loadMyRepos($scope.namespace);
|
||||
});
|
||||
|
||||
|
|
|
@ -4,9 +4,15 @@
|
|||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('new-organization', 'new-organization.html', NewOrgCtrl, {
|
||||
'newLayout': true,
|
||||
'title': 'New Organization',
|
||||
'description': 'Create a new organization to manage teams and permissions'
|
||||
});
|
||||
}, ['layout']);
|
||||
|
||||
pages.create('new-organization', 'old-new-organization.html', NewOrgCtrl, {
|
||||
'title': 'New Organization',
|
||||
'description': 'Create a new organization to manage teams and permissions'
|
||||
}, ['old-layout']);
|
||||
}]);
|
||||
|
||||
function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, PlanService, ApiService, CookieService, Features) {
|
||||
|
|
|
@ -3,10 +3,16 @@
|
|||
* Page to create a new repository.
|
||||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('new-repo', 'new-repo.html', NewRepoCtrl, {
|
||||
pages.create('new-repo', 'new-repo.html', NewRepoCtrl, {
|
||||
'newLayout': true,
|
||||
'title': 'New Repository',
|
||||
'description': 'Create a new Docker repository'
|
||||
});
|
||||
}, ['layout'])
|
||||
|
||||
pages.create('new-repo', 'old-new-repo.html', NewRepoCtrl, {
|
||||
'title': 'New Repository',
|
||||
'description': 'Create a new Docker repository'
|
||||
}, ['old-layout']);
|
||||
}]);
|
||||
|
||||
function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService, PlanService, TriggerService, Features) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
(function() {
|
||||
/**
|
||||
* Organization admin/settings page.
|
||||
* DEPRECATED: Organization admin/settings page.
|
||||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('org-admin', 'org-admin.html', OrgAdminCtrl);
|
||||
|
|
|
@ -3,10 +3,95 @@
|
|||
* Page that displays details about an organization, such as its teams.
|
||||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('org-view', 'org-view.html', OrgViewCtrl);
|
||||
pages.create('org-view', 'org-view.html', OrgViewCtrl, {
|
||||
'newLayout': true,
|
||||
'title': 'Organization {{ organization.name }}',
|
||||
'description': 'Organization {{ organization.name }}'
|
||||
}, ['layout'])
|
||||
|
||||
pages.create('org-view', 'old-org-view.html', OldOrgViewCtrl, {
|
||||
}, ['old-layout']);
|
||||
}]);
|
||||
|
||||
function OrgViewCtrl($rootScope, $scope, ApiService, $routeParams, CreateService) {
|
||||
function OrgViewCtrl($scope, $routeParams, $timeout, ApiService, UIService, AvatarService) {
|
||||
var orgname = $routeParams.orgname;
|
||||
|
||||
$scope.showLogsCounter = 0;
|
||||
$scope.showApplicationsCounter = 0;
|
||||
$scope.showInvoicesCounter = 0;
|
||||
|
||||
$scope.orgScope = {
|
||||
'changingOrganization': false,
|
||||
'organizationEmail': ''
|
||||
};
|
||||
|
||||
$scope.$watch('orgScope.organizationEmail', function(e) {
|
||||
UIService.hidePopover('#changeEmailForm input');
|
||||
});
|
||||
|
||||
var loadRepositories = function() {
|
||||
var options = {
|
||||
'namespace_only': true,
|
||||
'namespace': orgname,
|
||||
};
|
||||
|
||||
$scope.repositoriesResource = ApiService.listReposAsResource().withOptions(options).get(function(resp) {
|
||||
return resp.repositories;
|
||||
});
|
||||
};
|
||||
|
||||
var loadOrganization = function() {
|
||||
$scope.orgResource = ApiService.getOrganizationAsResource({'orgname': orgname}).get(function(org) {
|
||||
$scope.organization = org;
|
||||
$scope.orgScope.organizationEmail = org.email;
|
||||
$scope.isAdmin = org.is_admin;
|
||||
$scope.isMember = org.is_member;
|
||||
|
||||
// Load the repositories.
|
||||
$timeout(function() {
|
||||
loadRepositories();
|
||||
}, 10);
|
||||
});
|
||||
};
|
||||
|
||||
// Load the organization.
|
||||
loadOrganization();
|
||||
|
||||
$scope.showInvoices = function() {
|
||||
$scope.showInvoicesCounter++;
|
||||
};
|
||||
|
||||
$scope.showApplications = function() {
|
||||
$scope.showApplicationsCounter++;
|
||||
};
|
||||
|
||||
$scope.showLogs = function() {
|
||||
$scope.showLogsCounter++;
|
||||
};
|
||||
|
||||
$scope.changeEmail = function() {
|
||||
UIService.hidePopover('#changeEmailForm input');
|
||||
|
||||
$scope.orgScope.changingOrganization = true;
|
||||
var params = {
|
||||
'orgname': orgname
|
||||
};
|
||||
|
||||
var data = {
|
||||
'email': $scope.orgScope.organizationEmail
|
||||
};
|
||||
|
||||
ApiService.changeOrganizationDetails(data, params).then(function(org) {
|
||||
$scope.orgScope.changingOrganization = false;
|
||||
$scope.organization = org;
|
||||
}, function(result) {
|
||||
$scope.orgScope.changingOrganization = false;
|
||||
UIService.showFormError('#changeEmailForm input', result, 'right');
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function OldOrgViewCtrl($rootScope, $scope, ApiService, $routeParams, CreateService) {
|
||||
var orgname = $routeParams.orgname;
|
||||
|
||||
$scope.TEAM_PATTERN = TEAM_PATTERN;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
(function() {
|
||||
/**
|
||||
* Page which displays the list of organizations of which the user is a member.
|
||||
* DEPRECATED: Page which displays the list of organizations of which the user is a member.
|
||||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('organizations', 'organizations.html', OrgsCtrl, {
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
}, ['old-layout']);
|
||||
}]);
|
||||
|
||||
function RepoViewCtrl($scope, $routeParams, $location, ApiService, UserService, AngularPollChannel) {
|
||||
function RepoViewCtrl($scope, $routeParams, $location, $timeout, ApiService, UserService, AngularPollChannel) {
|
||||
$scope.namespace = $routeParams.namespace;
|
||||
$scope.name = $routeParams.name;
|
||||
|
||||
|
@ -63,12 +63,21 @@
|
|||
};
|
||||
|
||||
$scope.repositoryResource = ApiService.getRepoAsResource(params).get(function(repo) {
|
||||
$scope.repository = repo;
|
||||
$scope.viewScope.repository = repo;
|
||||
$scope.setTags($routeParams.tag);
|
||||
|
||||
// Track builds.
|
||||
buildPollChannel = AngularPollChannel.create($scope, loadRepositoryBuilds, 5000 /* 5s */);
|
||||
buildPollChannel.start();
|
||||
// Load the remainder of the data async, so we don't block the initial view from
|
||||
// showing.
|
||||
$timeout(function() {
|
||||
$scope.setTags($routeParams.tag);
|
||||
|
||||
// Load the images.
|
||||
loadImages();
|
||||
|
||||
// Track builds.
|
||||
buildPollChannel = AngularPollChannel.create($scope, loadRepositoryBuilds, 5000 /* 5s */);
|
||||
buildPollChannel.start();
|
||||
}, 10);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -98,9 +107,8 @@
|
|||
}, errorHandler);
|
||||
};
|
||||
|
||||
// Load the repository and images.
|
||||
// Load the repository.
|
||||
loadRepository();
|
||||
loadImages();
|
||||
|
||||
$scope.setTags = function(tagNames) {
|
||||
if (!tagNames) {
|
||||
|
@ -638,6 +646,10 @@
|
|||
$scope.setImage($routeParams.image);
|
||||
}
|
||||
|
||||
$timeout(function() {
|
||||
$scope.tree.notifyResized();
|
||||
}, 100);
|
||||
|
||||
return resp.images;
|
||||
});
|
||||
};
|
||||
|
|
|
@ -140,7 +140,7 @@
|
|||
$scope.showSuperuserPanel = function() {
|
||||
$('#setupModal').modal('hide');
|
||||
var prefix = $scope.hasSSL ? 'https' : 'http';
|
||||
var hostname = $scope.hostname;
|
||||
var hostname = $scope.hostname || document.location.hostname;
|
||||
window.location = prefix + '://' + hostname + '/superuser';
|
||||
};
|
||||
|
||||
|
|
|
@ -3,7 +3,14 @@
|
|||
* Page to view the members of a team and add/remove them.
|
||||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('team-view', 'team-view.html', TeamViewCtrl);
|
||||
pages.create('team-view', 'team-view.html', TeamViewCtrl, {
|
||||
'newLayout': true,
|
||||
'title': 'Team {{ teamname }}',
|
||||
'description': 'Team {{ teamname }}'
|
||||
}, ['layout'])
|
||||
|
||||
pages.create('team-view', 'old-team-view.html', TeamViewCtrl, {
|
||||
}, ['old-layout']);
|
||||
}]);
|
||||
|
||||
function TeamViewCtrl($rootScope, $scope, $timeout, Features, Restangular, ApiService, $routeParams) {
|
||||
|
|
|
@ -4,9 +4,13 @@
|
|||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('tutorial', 'tutorial.html', TutorialCtrl, {
|
||||
'newLayout': true,
|
||||
'title': 'Tutorial',
|
||||
'description': 'Basic tutorial on using Docker with Quay.io'
|
||||
});
|
||||
'description': 'Basic tutorial on using Quay.io'
|
||||
}, ['layout'])
|
||||
|
||||
pages.create('tutorial', 'old-tutorial.html', TutorialCtrl, {
|
||||
}, ['old-layout']);
|
||||
}]);
|
||||
|
||||
function TutorialCtrl($scope, AngularTour, AngularTourSignals, UserService, Config) {
|
||||
|
@ -59,7 +63,7 @@
|
|||
'templateUrl': '/static/tutorial/push-image.html',
|
||||
'signal': AngularTourSignals.serverEvent('/realtime/user/subscribe?events=docker-cli',
|
||||
function(message, tourScope) {
|
||||
var pushing = message['data']['action'] == 'push_repo';
|
||||
var pushing = message['data']['action'] == 'push_start';
|
||||
if (pushing) {
|
||||
tourScope.repoName = message['data']['repository'];
|
||||
}
|
||||
|
@ -73,7 +77,7 @@
|
|||
'templateUrl': '/static/tutorial/pushing.html',
|
||||
'signal': AngularTourSignals.serverEvent('/realtime/user/subscribe?events=docker-cli',
|
||||
function(message, tourScope) {
|
||||
return message['data']['action'] == 'pushed_repo';
|
||||
return message['data']['action'] == 'push_repo';
|
||||
}),
|
||||
'waitMessage': "Waiting for repository push to complete"
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
(function() {
|
||||
/**
|
||||
* User admin/settings page.
|
||||
* DEPRECATED: User admin/settings page.
|
||||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('user-admin', 'user-admin.html', UserAdminCtrl, {
|
||||
|
@ -160,11 +160,7 @@
|
|||
$scope.updatingUser = false;
|
||||
$scope.changeEmailSent = true;
|
||||
$scope.sentEmail = $scope.cuser.email;
|
||||
|
||||
// Reset the form.
|
||||
delete $scope.cuser['email'];
|
||||
|
||||
$scope.changeEmailForm.$setPristine();
|
||||
}, function(result) {
|
||||
$scope.updatingUser = false;
|
||||
UIService.showFormError('#changeEmailForm', result);
|
||||
|
@ -196,6 +192,21 @@
|
|||
});
|
||||
};
|
||||
|
||||
$scope.generateClientToken = function() {
|
||||
var generateToken = function(password) {
|
||||
var data = {
|
||||
'password': password
|
||||
};
|
||||
|
||||
ApiService.generateUserClientKey(data).then(function(resp) {
|
||||
$scope.generatedClientToken = resp['key'];
|
||||
$('#clientTokenModal').modal({});
|
||||
}, ApiService.errorDisplay('Could not generate token'));
|
||||
};
|
||||
|
||||
UIService.showPasswordDialog('Enter your password to generated an encrypted version:', generateToken);
|
||||
};
|
||||
|
||||
$scope.detachExternalLogin = function(kind) {
|
||||
var params = {
|
||||
'servicename': kind
|
||||
|
|
112
static/js/pages/user-view.js
Normal file
112
static/js/pages/user-view.js
Normal file
|
@ -0,0 +1,112 @@
|
|||
(function() {
|
||||
/**
|
||||
* Page that displays details about an user.
|
||||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('user-view', 'user-view.html', UserViewCtrl, {
|
||||
'newLayout': true,
|
||||
'title': 'User {{ user.username }}',
|
||||
'description': 'User {{ user.username }}'
|
||||
}, ['layout'])
|
||||
}]);
|
||||
|
||||
function UserViewCtrl($scope, $routeParams, $timeout, ApiService, UserService, UIService, AvatarService) {
|
||||
var username = $routeParams.username;
|
||||
|
||||
$scope.showInvoicesCounter = 0;
|
||||
$scope.showAppsCounter = 0;
|
||||
$scope.changeEmailInfo = {};
|
||||
$scope.changePasswordInfo = {};
|
||||
|
||||
UserService.updateUserIn($scope);
|
||||
|
||||
var loadRepositories = function() {
|
||||
var options = {
|
||||
'sort': true,
|
||||
'namespace_only': true,
|
||||
'namespace': username,
|
||||
};
|
||||
|
||||
$scope.repositoriesResource = ApiService.listReposAsResource().withOptions(options).get(function(resp) {
|
||||
return resp.repositories;
|
||||
});
|
||||
};
|
||||
|
||||
var loadUser = function() {
|
||||
$scope.userResource = ApiService.getUserInformationAsResource({'username': username}).get(function(user) {
|
||||
$scope.viewuser = user;
|
||||
|
||||
// Load the repositories.
|
||||
$timeout(function() {
|
||||
loadRepositories();
|
||||
}, 10);
|
||||
});
|
||||
};
|
||||
|
||||
// Load the user.
|
||||
loadUser();
|
||||
|
||||
$scope.showInvoices = function() {
|
||||
$scope.showInvoicesCounter++;
|
||||
};
|
||||
|
||||
$scope.showApplications = function() {
|
||||
$scope.showAppsCounter++;
|
||||
};
|
||||
|
||||
$scope.changePassword = function() {
|
||||
UIService.hidePopover('#changePasswordForm');
|
||||
$scope.changePasswordInfo.state = 'changing';
|
||||
|
||||
var data = {
|
||||
'password': $scope.changePasswordInfo.password
|
||||
};
|
||||
|
||||
ApiService.changeUserDetails(data).then(function(resp) {
|
||||
$scope.changePasswordInfo.state = 'changed';
|
||||
|
||||
// Reset the form
|
||||
delete $scope.changePasswordInfo['password']
|
||||
delete $scope.changePasswordInfo['repeatPassword']
|
||||
|
||||
// Reload the user.
|
||||
UserService.load();
|
||||
}, function(result) {
|
||||
$scope.changePasswordInfo.state = 'change-error';
|
||||
UIService.showFormError('#changePasswordForm', result);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.generateClientToken = function() {
|
||||
var generateToken = function(password) {
|
||||
var data = {
|
||||
'password': password
|
||||
};
|
||||
|
||||
ApiService.generateUserClientKey(data).then(function(resp) {
|
||||
$scope.generatedClientToken = resp['key'];
|
||||
$('#clientTokenModal').modal({});
|
||||
}, ApiService.errorDisplay('Could not generate token'));
|
||||
};
|
||||
|
||||
UIService.showPasswordDialog('Enter your password to generated an encrypted version:', generateToken);
|
||||
};
|
||||
|
||||
$scope.changeEmail = function() {
|
||||
UIService.hidePopover('#changeEmailForm');
|
||||
|
||||
var details = {
|
||||
'email': $scope.changeEmailInfo.email
|
||||
};
|
||||
|
||||
$scope.changeEmailInfo.state = 'sending';
|
||||
ApiService.changeUserDetails(details).then(function() {
|
||||
$scope.changeEmailInfo.state = 'sent';
|
||||
delete $scope.changeEmailInfo['email'];
|
||||
}, function(result) {
|
||||
$scope.changeEmailInfo.state = 'send-error';
|
||||
UIService.showFormError('#changeEmailForm', result);
|
||||
});
|
||||
};
|
||||
}
|
||||
})();
|
4
static/js/services/angular-view-array.js
vendored
4
static/js/services/angular-view-array.js
vendored
|
@ -29,7 +29,7 @@ angular.module('quay').factory('AngularViewArray', ['$interval', function($inter
|
|||
this.hasEntries = true;
|
||||
|
||||
if (this.isVisible) {
|
||||
this.setVisible(true);
|
||||
this.startTimer_();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -64,6 +64,8 @@ angular.module('quay').factory('AngularViewArray', ['$interval', function($inter
|
|||
};
|
||||
|
||||
_ViewArray.prototype.startTimer_ = function() {
|
||||
if (this.timerRef_) { return; }
|
||||
|
||||
var that = this;
|
||||
this.timerRef_ = $interval(function() {
|
||||
that.showAdditionalEntries_();
|
||||
|
|
|
@ -14,7 +14,9 @@ angular.module('quay').factory('AvatarService', ['Config', '$sanitize', 'md5',
|
|||
break;
|
||||
|
||||
case 'gravatar':
|
||||
return '//www.gravatar.com/avatar/' + hash + '?d=identicon&size=' + size;
|
||||
// TODO(jschorr): Remove once the new layout is in place everywhere.
|
||||
var default_kind = Config.isNewLayout() ? '404' : 'identicon';
|
||||
return '//www.gravatar.com/avatar/' + hash + '?d=' + default_kind + '&size=' + size;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* Service which exposes various methods for creating entities on the backend.
|
||||
*/
|
||||
angular.module('quay').factory('CreateService', ['ApiService', function(ApiService) {
|
||||
angular.module('quay').factory('CreateService', ['ApiService', 'UserService', function(ApiService, UserService) {
|
||||
var createService = {};
|
||||
|
||||
createService.createRobotAccount = function(ApiService, is_org, orgname, name, callback) {
|
||||
|
@ -24,5 +24,38 @@ angular.module('quay').factory('CreateService', ['ApiService', function(ApiServi
|
|||
.then(callback, ApiService.errorDisplay('Cannot create team'));
|
||||
};
|
||||
|
||||
createService.askCreateRobot = function(namespace, callback) {
|
||||
if (!namespace || !UserService.isNamespaceAdmin(namespace)) { return; }
|
||||
|
||||
var isorg = UserService.isOrganization(namespace);
|
||||
bootbox.prompt('Enter the name of the new robot account', function(robotname) {
|
||||
if (!robotname) { return; }
|
||||
|
||||
var regex = new RegExp(ROBOT_PATTERN);
|
||||
if (!regex.test(robotname)) {
|
||||
bootbox.alert('Invalid robot account name');
|
||||
return;
|
||||
}
|
||||
|
||||
createService.createRobotAccount(ApiService, isorg, namespace, robotname, callback);
|
||||
});
|
||||
};
|
||||
|
||||
createService.askCreateTeam = function(namespace, callback) {
|
||||
if (!namespace || !UserService.isNamespaceAdmin(namespace)) { return; }
|
||||
|
||||
bootbox.prompt('Enter the name of the new team', function(teamname) {
|
||||
if (!teamname) { return; }
|
||||
|
||||
var regex = new RegExp(TEAM_PATTERN);
|
||||
if (!regex.test(teamname)) {
|
||||
bootbox.alert('Invalid team name');
|
||||
return;
|
||||
}
|
||||
|
||||
CreateService.createOrganizationTeam(ApiService, namespace, teamname, callback);
|
||||
});
|
||||
};
|
||||
|
||||
return createService;
|
||||
}]);
|
||||
|
|
|
@ -54,6 +54,10 @@ angular.module('quay').factory('Config', [function() {
|
|||
return config['PREFERRED_URL_SCHEME'] + '://' + auth + config['SERVER_HOSTNAME'];
|
||||
};
|
||||
|
||||
config.getHttp = function() {
|
||||
return config['PREFERRED_URL_SCHEME'];
|
||||
};
|
||||
|
||||
config.getUrl = function(opt_path) {
|
||||
var path = opt_path || '';
|
||||
return config['PREFERRED_URL_SCHEME'] + '://' + config['SERVER_HOSTNAME'] + path;
|
||||
|
@ -67,5 +71,10 @@ angular.module('quay').factory('Config', [function() {
|
|||
return value;
|
||||
};
|
||||
|
||||
config.isNewLayout = function() {
|
||||
// TODO(jschorr): Remove once new layout is in place for everyone.
|
||||
return document.cookie.toString().indexOf('quay.exp-new-layout=true') >= 0;
|
||||
};
|
||||
|
||||
return config;
|
||||
}]);
|
|
@ -24,6 +24,10 @@ angular.module('quay').factory('KeyService', ['$location', 'Config', function($l
|
|||
keyService['githubTriggerAuthorizeUrl'] = oauth['GITHUB_TRIGGER_CONFIG']['AUTHORIZE_ENDPOINT'];
|
||||
|
||||
keyService['githubLoginScope'] = 'user:email';
|
||||
if (oauth['GITHUB_LOGIN_CONFIG']['ORG_RESTRICT']) {
|
||||
keyService['githubLoginScope'] += ',read:org';
|
||||
}
|
||||
|
||||
keyService['googleLoginScope'] = 'openid email';
|
||||
|
||||
keyService.isEnterprise = function(service) {
|
||||
|
|
|
@ -66,10 +66,10 @@ angular.module('quay').factory('UIService', [function() {
|
|||
}
|
||||
};
|
||||
|
||||
uiService.showPopover = function(elem, content) {
|
||||
uiService.showPopover = function(elem, content, opt_placement) {
|
||||
var popover = $(elem).data('bs.popover');
|
||||
if (!popover) {
|
||||
$(elem).popover({'content': '-', 'placement': 'left'});
|
||||
$(elem).popover({'content': '-', 'placement': opt_placement || 'left'});
|
||||
}
|
||||
|
||||
setTimeout(function() {
|
||||
|
@ -79,10 +79,10 @@ angular.module('quay').factory('UIService', [function() {
|
|||
}, 500);
|
||||
};
|
||||
|
||||
uiService.showFormError = function(elem, result) {
|
||||
uiService.showFormError = function(elem, result, opt_placement) {
|
||||
var message = result.data['message'] || result.data['error_description'] || '';
|
||||
if (message) {
|
||||
uiService.showPopover(elem, message);
|
||||
uiService.showPopover(elem, message, opt_placement);
|
||||
} else {
|
||||
uiService.hidePopover(elem);
|
||||
}
|
||||
|
@ -92,5 +92,47 @@ angular.module('quay').factory('UIService', [function() {
|
|||
return new CheckStateController(items, opt_checked);
|
||||
};
|
||||
|
||||
uiService.showPasswordDialog = function(message, callback, opt_canceledCallback) {
|
||||
var success = function() {
|
||||
var password = $('#passDialogBox').val();
|
||||
$('#passDialogBox').val('');
|
||||
callback(password);
|
||||
};
|
||||
|
||||
var canceled = function() {
|
||||
$('#passDialogBox').val('');
|
||||
opt_canceledCallback && opt_canceledCallback();
|
||||
};
|
||||
|
||||
var box = bootbox.dialog({
|
||||
"message": message +
|
||||
'<form style="margin-top: 10px" action="javascript:void(0)">' +
|
||||
'<input id="passDialogBox" class="form-control" type="password" placeholder="Current Password">' +
|
||||
'</form>',
|
||||
"title": 'Please Verify',
|
||||
"buttons": {
|
||||
"verify": {
|
||||
"label": "Verify",
|
||||
"className": "btn-success",
|
||||
"callback": success
|
||||
},
|
||||
"close": {
|
||||
"label": "Cancel",
|
||||
"className": "btn-default",
|
||||
"callback": canceled
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
box.bind('shown.bs.modal', function(){
|
||||
box.find("input").focus();
|
||||
box.find("form").submit(function() {
|
||||
if (!$('#passDialogBox').val()) { return; }
|
||||
box.modal('hide');
|
||||
success();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return uiService;
|
||||
}]);
|
||||
|
|
|
@ -12,7 +12,8 @@ function(ApiService, CookieService, $rootScope, Config) {
|
|||
username: null,
|
||||
email: null,
|
||||
organizations: [],
|
||||
logins: []
|
||||
logins: [],
|
||||
beforeload: true
|
||||
}
|
||||
|
||||
var userService = {}
|
||||
|
@ -83,6 +84,10 @@ function(ApiService, CookieService, $rootScope, Config) {
|
|||
});
|
||||
};
|
||||
|
||||
userService.isOrganization = function(name) {
|
||||
return !!userService.getOrganization(name);
|
||||
};
|
||||
|
||||
userService.getOrganization = function(name) {
|
||||
if (!userResponse || !userResponse.organizations) { return null; }
|
||||
for (var i = 0; i < userResponse.organizations.length; ++i) {
|
||||
|
|
Reference in a new issue