Merge branch 'master' into git

This commit is contained in:
Jimmy Zelinskie 2015-04-16 17:38:35 -04:00
commit ba2cb08904
268 changed files with 7008 additions and 1535 deletions

View file

@ -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;

View file

@ -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();
});
};

View 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});
});
}
}
};
});

View file

@ -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});
};

View file

@ -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++;
};

View file

@ -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();

View file

@ -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;

View 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;
});

View 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;
});

View file

@ -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;

View file

@ -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;
});
};

View file

@ -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) {

View 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;
});

View file

@ -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('+');

View file

@ -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) {

View file

@ -11,6 +11,7 @@ angular.module('quay').directive('externalLoginButton', function () {
scope: {
'signInStarted': '&signInStarted',
'redirectUrl': '=redirectUrl',
'isLink': '=isLink',
'provider': '@provider',
'action': '@action'
},

View 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;
});

View 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;
});

View file

@ -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;

View 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;
});

View 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;
});

View file

@ -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) {

View file

@ -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)
};

View file

@ -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);

View file

@ -12,6 +12,7 @@ angular.module('quay').directive('roleGroup', function () {
scope: {
'roles': '=roles',
'currentRole': '=currentRole',
'readOnly': '=readOnly',
'roleChanged': '&roleChanged'
},
controller: function($scope, $element) {

View 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;
});

View file

@ -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_);
};

View file

@ -14,6 +14,7 @@
$scope.setEnabled = function(value) {
$scope.isEnabled = value;
CookieService.putPermanent('quay.exp-new-layout', value.toString());
document.location.reload();
};
}
}());

View file

@ -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) {

View file

@ -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);
});

View file

@ -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) {

View file

@ -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) {

View file

@ -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);

View file

@ -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;

View file

@ -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, {

View file

@ -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;
});
};

View file

@ -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';
};

View file

@ -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) {

View file

@ -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"
},

View file

@ -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

View 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);
});
};
}
})();

View file

@ -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_();

View file

@ -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;
}
};

View file

@ -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;
}]);

View file

@ -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;
}]);

View file

@ -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) {

View file

@ -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;
}]);

View file

@ -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) {