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

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