Code cleanup part #1: move all the services and directive JS code in the app.js file into its own files

This commit is contained in:
Joseph Schorr 2015-02-19 16:21:54 -05:00
parent 3cae6609a7
commit 9b87999c1c
97 changed files with 7076 additions and 6870 deletions

42
static/js/services/angular-helper.js vendored Normal file
View file

@ -0,0 +1,42 @@
/**
* Helper code for working with angular.
*/
angular.module('quay').factory('AngularHelper', [function() {
var helper = {};
helper.buildConditionalLinker = function($animate, name, evaluator) {
// Based off of a solution found here: http://stackoverflow.com/questions/20325480/angularjs-whats-the-best-practice-to-add-ngif-to-a-directive-programmatically
return function ($scope, $element, $attr, ctrl, $transclude) {
var block;
var childScope;
var roles;
$attr.$observe(name, function (value) {
if (evaluator($scope.$eval(value))) {
if (!childScope) {
childScope = $scope.$new();
$transclude(childScope, function (clone) {
block = {
startNode: clone[0],
endNode: clone[clone.length++] = document.createComment(' end ' + name + ': ' + $attr[name] + ' ')
};
$animate.enter(clone, $element.parent(), $element);
});
}
} else {
if (childScope) {
childScope.$destroy();
childScope = null;
}
if (block) {
$animate.leave(getBlockElements(block));
block = null;
}
}
});
}
};
return helper;
}]);

View file

@ -0,0 +1,72 @@
/**
* Specialized class for conducting an HTTP poll, while properly preventing multiple calls.
*/
angular.module('quay').factory('AngularPollChannel', ['ApiService', '$timeout', function(ApiService, $timeout) {
var _PollChannel = function(scope, requester, opt_sleeptime) {
this.scope_ = scope;
this.requester_ = requester;
this.sleeptime_ = opt_sleeptime || (60 * 1000 /* 60s */);
this.timer_ = null;
this.working = false;
this.polling = false;
var that = this;
scope.$on('$destroy', function() {
that.stop();
});
};
_PollChannel.prototype.stop = function() {
if (this.timer_) {
$timeout.cancel(this.timer_);
this.timer_ = null;
this.polling_ = false;
}
this.working = false;
};
_PollChannel.prototype.start = function() {
// Make sure we invoke call outside the normal digest cycle, since
// we'll call $scope.$apply ourselves.
var that = this;
setTimeout(function() { that.call_(); }, 0);
};
_PollChannel.prototype.call_ = function() {
if (this.working) { return; }
var that = this;
this.working = true;
this.scope_.$apply(function() {
that.requester_(function(status) {
if (status) {
that.working = false;
that.setupTimer_();
} else {
that.stop();
}
});
});
};
_PollChannel.prototype.setupTimer_ = function() {
if (this.timer_) { return; }
var that = this;
this.polling = true;
this.timer_ = $timeout(function() {
that.timer_ = null;
that.call_();
}, this.sleeptime_)
};
var service = {
'create': function(scope, requester, opt_sleeptime) {
return new _PollChannel(scope, requester, opt_sleeptime);
}
};
return service;
}]);

View file

@ -0,0 +1,87 @@
/**
* Specialized wrapper around array which provides a toggle() method for viewing the contents of the
* array in a manner that is asynchronously filled in over a short time period. This prevents long
* pauses in the UI for ngRepeat's when the array is significant in size.
*/
angular.module('quay').factory('AngularViewArray', ['$interval', function($interval) {
var ADDTIONAL_COUNT = 20;
function _ViewArray() {
this.isVisible = false;
this.visibleEntries = null;
this.hasEntries = false;
this.entries = [];
this.timerRef_ = null;
this.currentIndex_ = 0;
}
_ViewArray.prototype.length = function() {
return this.entries.length;
};
_ViewArray.prototype.get = function(index) {
return this.entries[index];
};
_ViewArray.prototype.push = function(elem) {
this.entries.push(elem);
this.hasEntries = true;
if (this.isVisible) {
this.setVisible(true);
}
};
_ViewArray.prototype.toggle = function() {
this.setVisible(!this.isVisible);
};
_ViewArray.prototype.setVisible = function(newState) {
this.isVisible = newState;
this.visibleEntries = [];
this.currentIndex_ = 0;
if (newState) {
this.showAdditionalEntries_();
this.startTimer_();
} else {
this.stopTimer_();
}
};
_ViewArray.prototype.showAdditionalEntries_ = function() {
var i = 0;
for (i = this.currentIndex_; i < (this.currentIndex_ + ADDTIONAL_COUNT) && i < this.entries.length; ++i) {
this.visibleEntries.push(this.entries[i]);
}
this.currentIndex_ = i;
if (this.currentIndex_ >= this.entries.length) {
this.stopTimer_();
}
};
_ViewArray.prototype.startTimer_ = function() {
var that = this;
this.timerRef_ = $interval(function() {
that.showAdditionalEntries_();
}, 10);
};
_ViewArray.prototype.stopTimer_ = function() {
if (this.timerRef_) {
$interval.cancel(this.timerRef_);
this.timerRef_ = null;
}
};
var service = {
'create': function() {
return new _ViewArray();
}
};
return service;
}]);

View file

@ -0,0 +1,330 @@
/**
* Service which exposes the server-defined API as a nice set of helper methods and automatic
* callbacks. Any method defined on the server is exposed here as an equivalent method. Also
* defines some helper functions for working with API responses.
*/
angular.module('quay').factory('ApiService', ['Restangular', '$q', function(Restangular, $q) {
var apiService = {};
var getResource = function(path, opt_background) {
var resource = {};
resource.url = path;
resource.withOptions = function(options) {
this.options = options;
return this;
};
resource.get = function(processor, opt_errorHandler) {
var options = this.options;
var performer = Restangular.one(this.url);
var result = {
'loading': true,
'value': null,
'hasError': false
};
if (opt_background) {
performer.withHttpConfig({
'ignoreLoadingBar': true
});
}
performer.get(options).then(function(resp) {
result.value = processor(resp);
result.loading = false;
}, function(resp) {
result.hasError = true;
result.loading = false;
if (opt_errorHandler) {
opt_errorHandler(resp);
}
});
return result;
};
return resource;
};
var buildUrl = function(path, parameters, opt_forcessl) {
// We already have /api/v1/ on the URLs, so remove them from the paths.
path = path.substr('/api/v1/'.length, path.length);
// Build the path, adjusted with the inline parameters.
var used = {};
var url = '';
for (var i = 0; i < path.length; ++i) {
var c = path[i];
if (c == '{') {
var end = path.indexOf('}', i);
var varName = path.substr(i + 1, end - i - 1);
if (!parameters[varName]) {
throw new Error('Missing parameter: ' + varName);
}
used[varName] = true;
url += parameters[varName];
i = end;
continue;
}
url += c;
}
// Append any query parameters.
var isFirst = true;
for (var paramName in parameters) {
if (!parameters.hasOwnProperty(paramName)) { continue; }
if (used[paramName]) { continue; }
var value = parameters[paramName];
if (value) {
url += isFirst ? '?' : '&';
url += paramName + '=' + encodeURIComponent(value)
isFirst = false;
}
}
// If we are forcing SSL, return an absolutel URL with an SSL prefix.
if (opt_forcessl) {
path = 'https://' + window.location.host + '/api/v1/' + path;
}
return url;
};
var getGenericOperationName = function(userOperationName) {
return userOperationName.replace('User', '');
};
var getMatchingUserOperationName = function(orgOperationName, method, userRelatedResource) {
if (userRelatedResource) {
var operations = userRelatedResource['operations'];
for (var i = 0; i < operations.length; ++i) {
var operation = operations[i];
if (operation['method'].toLowerCase() == method) {
return operation['nickname'];
}
}
}
throw new Error('Could not find user operation matching org operation: ' + orgOperationName);
};
var buildMethodsForEndpointResource = function(endpointResource, resourceMap) {
var name = endpointResource['name'];
var operations = endpointResource['operations'];
for (var i = 0; i < operations.length; ++i) {
var operation = operations[i];
buildMethodsForOperation(operation, endpointResource, resourceMap);
}
};
var freshLoginInProgress = [];
var reject = function(msg) {
for (var i = 0; i < freshLoginInProgress.length; ++i) {
freshLoginInProgress[i].deferred.reject({'data': {'message': msg}});
}
freshLoginInProgress = [];
};
var retry = function() {
for (var i = 0; i < freshLoginInProgress.length; ++i) {
freshLoginInProgress[i].retry();
}
freshLoginInProgress = [];
};
var freshLoginFailCheck = function(opName, opArgs) {
return function(resp) {
var deferred = $q.defer();
// If the error is a fresh login required, show the dialog.
if (resp.status == 401 && resp.data['error_type'] == 'fresh_login_required') {
var retryOperation = function() {
apiService[opName].apply(apiService, opArgs).then(function(resp) {
deferred.resolve(resp);
}, function(resp) {
deferred.reject(resp);
});
};
var verifyNow = function() {
var info = {
'password': $('#freshPassword').val()
};
$('#freshPassword').val('');
// Conduct the sign in of the user.
apiService.verifyUser(info).then(function() {
// On success, retry the operations. if it succeeds, then resolve the
// deferred promise with the result. Otherwise, reject the same.
retry();
}, function(resp) {
// Reject with the sign in error.
reject('Invalid verification credentials');
});
};
// Add the retry call to the in progress list. If there is more than a single
// in progress call, we skip showing the dialog (since it has already been
// shown).
freshLoginInProgress.push({
'deferred': deferred,
'retry': retryOperation
})
if (freshLoginInProgress.length > 1) {
return deferred.promise;
}
var box = bootbox.dialog({
"message": 'It has been more than a few minutes since you last logged in, ' +
'so please verify your password to perform this sensitive operation:' +
'<form style="margin-top: 10px" action="javascript:void(0)">' +
'<input id="freshPassword" class="form-control" type="password" placeholder="Current Password">' +
'</form>',
"title": 'Please Verify',
"buttons": {
"verify": {
"label": "Verify",
"className": "btn-success",
"callback": verifyNow
},
"close": {
"label": "Cancel",
"className": "btn-default",
"callback": function() {
reject('Verification canceled')
}
}
}
});
box.bind('shown.bs.modal', function(){
box.find("input").focus();
box.find("form").submit(function() {
if (!$('#freshPassword').val()) { return; }
box.modal('hide');
verifyNow();
});
});
// Return a new promise. We'll accept or reject it based on the result
// of the login.
return deferred.promise;
}
// Otherwise, we just 'raise' the error via the reject method on the promise.
return $q.reject(resp);
};
};
var buildMethodsForOperation = function(operation, resource, resourceMap) {
var method = operation['method'].toLowerCase();
var operationName = operation['nickname'];
var path = resource['path'];
// Add the operation itself.
apiService[operationName] = function(opt_options, opt_parameters, opt_background, opt_forcessl) {
var one = Restangular.one(buildUrl(path, opt_parameters, opt_forcessl));
if (opt_background) {
one.withHttpConfig({
'ignoreLoadingBar': true
});
}
var opObj = one['custom' + method.toUpperCase()](opt_options);
// If the operation requires_fresh_login, then add a specialized error handler that
// will defer the operation's result if sudo is requested.
if (operation['requires_fresh_login']) {
opObj = opObj.catch(freshLoginFailCheck(operationName, arguments));
}
return opObj;
};
// If the method for the operation is a GET, add an operationAsResource method.
if (method == 'get') {
apiService[operationName + 'AsResource'] = function(opt_parameters, opt_background) {
return getResource(buildUrl(path, opt_parameters), opt_background);
};
}
// If the resource has a user-related resource, then make a generic operation for this operation
// that can call both the user and the organization versions of the operation, depending on the
// parameters given.
if (resource['quayUserRelated']) {
var userOperationName = getMatchingUserOperationName(operationName, method, resourceMap[resource['quayUserRelated']]);
var genericOperationName = getGenericOperationName(userOperationName);
apiService[genericOperationName] = function(orgname, opt_options, opt_parameters, opt_background) {
if (orgname) {
if (orgname.name) {
orgname = orgname.name;
}
var params = jQuery.extend({'orgname' : orgname}, opt_parameters || {}, opt_background);
return apiService[operationName](opt_options, params);
} else {
return apiService[userOperationName](opt_options, opt_parameters, opt_background);
}
};
}
};
if (!window.__endpoints) {
return apiService;
}
var resourceMap = {};
// Build the map of resource names to their objects.
for (var i = 0; i < window.__endpoints.length; ++i) {
var endpointResource = window.__endpoints[i];
resourceMap[endpointResource['name']] = endpointResource;
}
// Construct the methods for each API endpoint.
for (var i = 0; i < window.__endpoints.length; ++i) {
var endpointResource = window.__endpoints[i];
buildMethodsForEndpointResource(endpointResource, resourceMap);
}
apiService.getErrorMessage = function(resp, defaultMessage) {
var message = defaultMessage;
if (resp['data']) {
message = resp['data']['error_message'] || resp['data']['message'] || resp['data']['error_description'] || message;
}
return message;
};
apiService.errorDisplay = function(defaultMessage, opt_handler) {
return function(resp) {
var message = apiService.getErrorMessage(resp, defaultMessage);
if (opt_handler) {
var handlerMessage = opt_handler(resp);
if (handlerMessage) {
message = handlerMessage;
}
}
bootbox.dialog({
"message": message,
"title": defaultMessage,
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
};
};
return apiService;
}]);

View file

@ -0,0 +1,48 @@
/**
* Service which provides helper methods for retrieving the avatars displayed in the app.
*/
angular.module('quay').factory('AvatarService', ['Config', '$sanitize', 'md5',
function(Config, $sanitize, md5) {
var avatarService = {};
var cache = {};
avatarService.getAvatar = function(hash, opt_size) {
var size = opt_size || 16;
switch (Config['AVATAR_KIND']) {
case 'local':
return '/avatar/' + hash + '?size=' + size;
break;
case 'gravatar':
return '//www.gravatar.com/avatar/' + hash + '?d=identicon&size=' + size;
break;
}
};
avatarService.computeHash = function(opt_email, opt_name) {
var email = opt_email || '';
var name = opt_name || '';
var cacheKey = email + ':' + name;
if (!cacheKey) { return '-'; }
if (cache[cacheKey]) {
return cache[cacheKey];
}
var hash = md5.createHash(email.toString().toLowerCase());
switch (Config['AVATAR_KIND']) {
case 'local':
if (name) {
hash = name[0] + hash;
} else if (email) {
hash = email[0] + hash;
}
break;
}
return cache[cacheKey] = hash;
};
return avatarService;
}]);

View file

@ -0,0 +1,36 @@
/**
* Helper service for working with the registry's container. Only works in enterprise.
*/
angular.module('quay').factory('ContainerService', ['ApiService', '$timeout',
function(ApiService, $timeout) {
var containerService = {};
containerService.restartContainer = function(callback) {
ApiService.scShutdownContainer(null, null).then(function(resp) {
$timeout(callback, 2000);
}, ApiService.errorDisplay('Cannot restart container. Please report this to support.'))
};
containerService.scheduleStatusCheck = function(callback) {
$timeout(function() {
containerService.checkStatus(callback);
}, 2000);
};
containerService.checkStatus = function(callback, force_ssl) {
var errorHandler = function(resp) {
if (resp.status == 404 || resp.status == 502) {
// Container has not yet come back up, so we schedule another check.
containerService.scheduleStatusCheck(callback);
return;
}
return ApiService.errorDisplay('Cannot load status. Please report this to support')(resp);
};
ApiService.scRegistryStatus(null, null)
.then(callback, errorHandler, /* background */true, /* force ssl*/force_ssl);
};
return containerService;
}]);

View file

@ -0,0 +1,23 @@
/**
* Helper service for working with cookies.
*/
angular.module('quay').factory('CookieService', ['$cookies', '$cookieStore', function($cookies, $cookieStore) {
var cookieService = {};
cookieService.putPermanent = function(name, value) {
document.cookie = escape(name) + "=" + escape(value) + "; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/";
};
cookieService.putSession = function(name, value) {
$cookies[name] = value;
};
cookieService.clear = function(name) {
$cookies[name] = '';
};
cookieService.get = function(name) {
return $cookies[name];
};
return cookieService;
}]);

View file

@ -0,0 +1,28 @@
/**
* Service which exposes various methods for creating entities on the backend.
*/
angular.module('quay').factory('CreateService', ['ApiService', function(ApiService) {
var createService = {};
createService.createRobotAccount = function(ApiService, is_org, orgname, name, callback) {
ApiService.createRobot(is_org ? orgname : null, null, {'robot_shortname': name})
.then(callback, ApiService.errorDisplay('Cannot create robot account'));
};
createService.createOrganizationTeam = function(ApiService, orgname, teamname, callback) {
var data = {
'name': teamname,
'role': 'member'
};
var params = {
'orgname': orgname,
'teamname': teamname
};
ApiService.updateOrganizationTeam(data, params)
.then(callback, ApiService.errorDisplay('Cannot create team'));
};
return createService;
}]);

View file

@ -0,0 +1,170 @@
/**
* Service which provides helper methods for downloading a data file from a URL, and extracting
* its contents as .tar, .tar.gz, or .zip file. Note that this service depends on external
* library code in the lib/ directory:
* - jszip.min.js
* - Blob.js
* - zlib.js
*/
angular.module('quay').factory('DataFileService', [function() {
var dataFileService = {};
dataFileService.getName_ = function(filePath) {
var parts = filePath.split('/');
return parts[parts.length - 1];
};
dataFileService.tryAsZip_ = function(buf, success, failure) {
var zip = null;
var zipFiles = null;
try {
var zip = new JSZip(buf);
zipFiles = zip.files;
} catch (e) {
failure();
return;
}
var files = [];
for (var filePath in zipFiles) {
if (zipFiles.hasOwnProperty(filePath)) {
files.push({
'name': dataFileService.getName_(filePath),
'path': filePath,
'canRead': true,
'toBlob': (function(fp) {
return function() {
return new Blob([zip.file(fp).asArrayBuffer()]);
};
}(filePath))
});
}
}
success(files);
};
dataFileService.tryAsTarGz_ = function(buf, success, failure) {
var gunzip = new Zlib.Gunzip(buf);
var plain = null;
try {
plain = gunzip.decompress();
} catch (e) {
failure();
return;
}
dataFileService.tryAsTar_(plain, success, failure);
};
dataFileService.tryAsTar_ = function(buf, success, failure) {
var collapsePath = function(originalPath) {
// Tar files can contain entries of the form './', so we need to collapse
// those paths down.
var parts = originalPath.split('/');
for (var i = parts.length - 1; i >= 0; i--) {
var part = parts[i];
if (part == '.') {
parts.splice(i, 1);
}
}
return parts.join('/');
};
var handler = new Untar(buf);
handler.process(function(status, read, files, err) {
switch (status) {
case 'error':
failure(err);
break;
case 'done':
var processed = [];
for (var i = 0; i < files.length; ++i) {
var currentFile = files[i];
var path = collapsePath(currentFile.meta.filename);
if (path == '' || path == 'pax_global_header') { continue; }
processed.push({
'name': dataFileService.getName_(path),
'path': path,
'canRead': true,
'toBlob': (function(currentFile) {
return function() {
return new Blob([currentFile.buffer], {type: 'application/octet-binary'});
};
}(currentFile))
});
}
success(processed);
break;
}
});
};
dataFileService.blobToString = function(blob, callback) {
var reader = new FileReader();
reader.onload = function(event){
callback(reader.result);
};
reader.readAsText(blob);
};
dataFileService.arrayToString = function(buf, callback) {
var bb = new Blob([buf], {type: 'application/octet-binary'});
var f = new FileReader();
f.onload = function(e) {
callback(e.target.result);
};
f.onerror = function(e) {
callback(null);
};
f.onabort = function(e) {
callback(null);
};
f.readAsText(bb);
};
dataFileService.readDataArrayAsPossibleArchive = function(buf, success, failure) {
dataFileService.tryAsZip_(buf, success, function() {
dataFileService.tryAsTarGz_(buf, success, failure);
});
};
dataFileService.downloadDataFileAsArrayBuffer = function($scope, url, progress, error, loaded) {
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'arraybuffer';
request.onprogress = function(e) {
$scope.$apply(function() {
var percentLoaded;
if (e.lengthComputable) {
progress(e.loaded / e.total);
}
});
};
request.onerror = function() {
$scope.$apply(function() {
error();
});
};
request.onload = function() {
if (this.status == 200) {
$scope.$apply(function() {
var uint8array = new Uint8Array(request.response);
loaded(uint8array);
});
return;
}
};
request.send();
};
return dataFileService;
}]);

View file

@ -0,0 +1,166 @@
/**
* Service which defines the various kinds of external notification and provides methods for
* easily looking up information about those kinds.
*/
angular.module('quay').factory('ExternalNotificationData', ['Config', 'Features',
function(Config, Features) {
var externalNotificationData = {};
var events = [
{
'id': 'repo_push',
'title': 'Push to Repository',
'icon': 'fa-upload'
}
];
if (Features.BUILD_SUPPORT) {
var buildEvents = [
{
'id': 'build_queued',
'title': 'Dockerfile Build Queued',
'icon': 'fa-tasks'
},
{
'id': 'build_start',
'title': 'Dockerfile Build Started',
'icon': 'fa-circle-o-notch'
},
{
'id': 'build_success',
'title': 'Dockerfile Build Successfully Completed',
'icon': 'fa-check-circle-o'
},
{
'id': 'build_failure',
'title': 'Dockerfile Build Failed',
'icon': 'fa-times-circle-o'
}];
for (var i = 0; i < buildEvents.length; ++i) {
events.push(buildEvents[i]);
}
}
var methods = [
{
'id': 'quay_notification',
'title': Config.REGISTRY_TITLE + ' Notification',
'icon': 'quay-icon',
'fields': [
{
'name': 'target',
'type': 'entity',
'title': 'Recipient'
}
]
},
{
'id': 'email',
'title': 'E-mail',
'icon': 'fa-envelope',
'fields': [
{
'name': 'email',
'type': 'email',
'title': 'E-mail address'
}
],
'enabled': Features.MAILING
},
{
'id': 'webhook',
'title': 'Webhook POST',
'icon': 'fa-link',
'fields': [
{
'name': 'url',
'type': 'url',
'title': 'Webhook URL'
}
]
},
{
'id': 'flowdock',
'title': 'Flowdock Team Notification',
'icon': 'flowdock-icon',
'fields': [
{
'name': 'flow_api_token',
'type': 'string',
'title': 'Flow API Token',
'help_url': 'https://www.flowdock.com/account/tokens'
}
]
},
{
'id': 'hipchat',
'title': 'HipChat Room Notification',
'icon': 'hipchat-icon',
'fields': [
{
'name': 'room_id',
'type': 'string',
'title': 'Room ID #'
},
{
'name': 'notification_token',
'type': 'string',
'title': 'Room Notification Token',
'help_url': 'https://hipchat.com/rooms/tokens/{room_id}'
}
]
},
{
'id': 'slack',
'title': 'Slack Room Notification',
'icon': 'slack-icon',
'fields': [
{
'name': 'url',
'type': 'regex',
'title': 'Webhook URL',
'regex': '^https://hooks\\.slack\\.com/services/[A-Z0-9]+/[A-Z0-9]+/[a-zA-Z0-9]+$',
'help_url': 'https://slack.com/services/new/incoming-webhook',
'placeholder': 'https://hooks.slack.com/service/{some}/{token}/{here}'
}
]
}
];
var methodMap = {};
var eventMap = {};
for (var i = 0; i < methods.length; ++i) {
methodMap[methods[i].id] = methods[i];
}
for (var i = 0; i < events.length; ++i) {
eventMap[events[i].id] = events[i];
}
externalNotificationData.getSupportedEvents = function() {
return events;
};
externalNotificationData.getSupportedMethods = function() {
var filtered = [];
for (var i = 0; i < methods.length; ++i) {
if (methods[i].enabled !== false) {
filtered.push(methods[i]);
}
}
return filtered;
};
externalNotificationData.getEventInfo = function(event) {
return eventMap[event];
};
externalNotificationData.getMethodInfo = function(method) {
return methodMap[method];
};
return externalNotificationData;
}]);

View file

@ -0,0 +1,71 @@
/**
* Feature flags.
*/
angular.module('quay').factory('Features', [function() {
if (!window.__features) {
return {};
}
var features = window.__features;
features.getFeature = function(name, opt_defaultValue) {
var value = features[name];
if (value == null) {
return opt_defaultValue;
}
return value;
};
features.hasFeature = function(name) {
return !!features.getFeature(name);
};
features.matchesFeatures = function(list) {
for (var i = 0; i < list.length; ++i) {
var value = features.getFeature(list[i]);
if (!value) {
return false;
}
}
return true;
};
return features;
}]);
/**
* Application configuration.
*/
angular.module('quay').factory('Config', [function() {
if (!window.__config) {
return {};
}
var config = window.__config;
config.getDomain = function() {
return config['SERVER_HOSTNAME'];
};
config.getHost = function(opt_auth) {
var auth = opt_auth;
if (auth) {
auth = auth + '@';
}
return config['PREFERRED_URL_SCHEME'] + '://' + auth + config['SERVER_HOSTNAME'];
};
config.getUrl = function(opt_path) {
var path = opt_path || '';
return config['PREFERRED_URL_SCHEME'] + '://' + config['SERVER_HOSTNAME'] + path;
};
config.getValue = function(name, opt_defaultValue) {
var value = config[name];
if (value == null) {
return opt_defaultValue;
}
return value;
};
return config;
}]);

View file

@ -0,0 +1,28 @@
/**
* Helper service for returning information extracted from repository image metadata.
*/
angular.module('quay').factory('ImageMetadataService', ['UtilService', function(UtilService) {
var metadataService = {};
metadataService.getFormattedCommand = function(image) {
if (!image || !image.command || !image.command.length) {
return '';
}
var getCommandStr = function(command) {
// Handle /bin/sh commands specially.
if (command.length > 2 && command[0] == '/bin/sh' && command[1] == '-c') {
return command[2];
}
return command.join(' ');
};
return getCommandStr(image.command);
};
metadataService.getEscapedFormattedCommand = function(image) {
return UtilService.textToSafeHtml(metadataService.getFormattedCommand(image));
};
return metadataService;
}]);

View file

@ -0,0 +1,63 @@
/**
* Service which provides access to the various keys defined in configuration, and working with
* external services that rely on those keys.
*/
angular.module('quay').factory('KeyService', ['$location', 'Config', function($location, Config) {
var keyService = {}
var oauth = window.__oauth;
keyService['stripePublishableKey'] = Config['STRIPE_PUBLISHABLE_KEY'];
keyService['githubTriggerClientId'] = oauth['GITHUB_TRIGGER_CONFIG']['CLIENT_ID'];
keyService['githubLoginClientId'] = oauth['GITHUB_LOGIN_CONFIG']['CLIENT_ID'];
keyService['googleLoginClientId'] = oauth['GOOGLE_LOGIN_CONFIG']['CLIENT_ID'];
keyService['githubRedirectUri'] = Config.getUrl('/oauth2/github/callback');
keyService['googleRedirectUri'] = Config.getUrl('/oauth2/google/callback');
keyService['githubLoginUrl'] = oauth['GITHUB_LOGIN_CONFIG']['AUTHORIZE_ENDPOINT'];
keyService['googleLoginUrl'] = oauth['GOOGLE_LOGIN_CONFIG']['AUTHORIZE_ENDPOINT'];
keyService['githubEndpoint'] = oauth['GITHUB_LOGIN_CONFIG']['GITHUB_ENDPOINT'];
keyService['githubTriggerEndpoint'] = oauth['GITHUB_TRIGGER_CONFIG']['GITHUB_ENDPOINT'];
keyService['githubTriggerAuthorizeUrl'] = oauth['GITHUB_TRIGGER_CONFIG']['AUTHORIZE_ENDPOINT'];
keyService['githubLoginScope'] = 'user:email';
keyService['googleLoginScope'] = 'openid email';
keyService.isEnterprise = function(service) {
switch (service) {
case 'github':
return keyService['githubLoginUrl'].indexOf('https://github.com/') < 0;
case 'github-trigger':
return keyService['githubTriggerAuthorizeUrl'].indexOf('https://github.com/') < 0;
}
return false;
};
keyService.getExternalLoginUrl = function(service, action) {
var state_clause = '';
if (Config.MIXPANEL_KEY && window.mixpanel) {
if (mixpanel.get_distinct_id !== undefined) {
state_clause = "&state=" + encodeURIComponent(mixpanel.get_distinct_id());
}
}
var client_id = keyService[service + 'LoginClientId'];
var scope = keyService[service + 'LoginScope'];
var redirect_uri = keyService[service + 'RedirectUri'];
if (action == 'attach') {
redirect_uri += '/attach';
}
var url = keyService[service + 'LoginUrl'] + 'client_id=' + client_id + '&scope=' + scope +
'&redirect_uri=' + redirect_uri + state_clause;
return url;
};
return keyService;
}]);

View file

@ -0,0 +1,228 @@
/**
* Service which defines the supported kinds of application notifications (those items that appear
* in the sidebar) and provides helper methods for working with them.
*/
angular.module('quay').factory('NotificationService',
['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', 'PlanService', 'UserService', 'Config', '$location',
function($rootScope, $interval, UserService, ApiService, StringBuilderService, PlanService, UserService, Config, $location) {
var notificationService = {
'user': null,
'notifications': [],
'notificationClasses': [],
'notificationSummaries': [],
'additionalNotifications': false
};
var pollTimerHandle = null;
var notificationKinds = {
'test_notification': {
'level': 'primary',
'message': 'This notification is a long message for testing: {obj}',
'page': '/about/',
'dismissable': true
},
'org_team_invite': {
'level': 'primary',
'message': '{inviter} is inviting you to join team {team} under organization {org}',
'actions': [
{
'title': 'Join team',
'kind': 'primary',
'handler': function(notification) {
window.location = '/confirminvite?code=' + notification.metadata['code'];
}
},
{
'title': 'Decline',
'kind': 'default',
'handler': function(notification) {
ApiService.declineOrganizationTeamInvite(null, {'code': notification.metadata['code']}).then(function() {
notificationService.update();
});
}
}
]
},
'password_required': {
'level': 'error',
'message': 'In order to begin pushing and pulling repositories, a password must be set for your account',
'page': '/user?tab=password'
},
'over_private_usage': {
'level': 'error',
'message': 'Namespace {namespace} is over its allowed private repository count. ' +
'<br><br>Please upgrade your plan to avoid disruptions in service.',
'page': function(metadata) {
var organization = UserService.getOrganization(metadata['namespace']);
if (organization) {
return '/organization/' + metadata['namespace'] + '/admin';
} else {
return '/user';
}
}
},
'expiring_license': {
'level': 'error',
'message': 'Your license will expire at: {expires_at} ' +
'<br><br>Please contact support to purchase a new license.',
'page': '/contact/'
},
'maintenance': {
'level': 'warning',
'message': 'We will be down for schedule maintenance from {from_date} to {to_date} ' +
'for {reason}. We are sorry about any inconvenience.',
'page': 'http://status.quay.io/'
},
'repo_push': {
'level': 'info',
'message': function(metadata) {
if (metadata.updated_tags && Object.getOwnPropertyNames(metadata.updated_tags).length) {
return 'Repository {repository} has been pushed with the following tags updated: {updated_tags}';
} else {
return 'Repository {repository} fhas been pushed';
}
},
'page': function(metadata) {
return '/repository/' + metadata.repository;
},
'dismissable': true
},
'build_queued': {
'level': 'info',
'message': 'A build has been queued for repository {repository}',
'page': function(metadata) {
return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
},
'dismissable': true
},
'build_start': {
'level': 'info',
'message': 'A build has been started for repository {repository}',
'page': function(metadata) {
return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
},
'dismissable': true
},
'build_success': {
'level': 'info',
'message': 'A build has succeeded for repository {repository}',
'page': function(metadata) {
return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
},
'dismissable': true
},
'build_failure': {
'level': 'error',
'message': 'A build has failed for repository {repository}',
'page': function(metadata) {
return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
},
'dismissable': true
}
};
notificationService.dismissNotification = function(notification) {
notification.dismissed = true;
var params = {
'uuid': notification.id
};
ApiService.updateUserNotification(notification, params, function() {
notificationService.update();
}, ApiService.errorDisplay('Could not update notification'));
var index = $.inArray(notification, notificationService.notifications);
if (index >= 0) {
notificationService.notifications.splice(index, 1);
}
};
notificationService.getActions = function(notification) {
var kindInfo = notificationKinds[notification['kind']];
if (!kindInfo) {
return [];
}
return kindInfo['actions'] || [];
};
notificationService.canDismiss = function(notification) {
var kindInfo = notificationKinds[notification['kind']];
if (!kindInfo) {
return false;
}
return !!kindInfo['dismissable'];
};
notificationService.getPage = function(notification) {
var kindInfo = notificationKinds[notification['kind']];
if (!kindInfo) {
return null;
}
var page = kindInfo['page'];
if (page != null && typeof page != 'string') {
page = page(notification['metadata']);
}
return page || '';
};
notificationService.getMessage = function(notification) {
var kindInfo = notificationKinds[notification['kind']];
if (!kindInfo) {
return '(Unknown notification kind: ' + notification['kind'] + ')';
}
return StringBuilderService.buildString(kindInfo['message'], notification['metadata']);
};
notificationService.getClass = function(notification) {
var kindInfo = notificationKinds[notification['kind']];
if (!kindInfo) {
return 'notification-info';
}
return 'notification-' + kindInfo['level'];
};
notificationService.getClasses = function(notifications) {
var classes = [];
for (var i = 0; i < notifications.length; ++i) {
var notification = notifications[i];
classes.push(notificationService.getClass(notification));
}
return classes.join(' ');
};
notificationService.update = function() {
var user = UserService.currentUser();
if (!user || user.anonymous) {
return;
}
ApiService.listUserNotifications().then(function(resp) {
notificationService.notifications = resp['notifications'];
notificationService.additionalNotifications = resp['additional'];
notificationService.notificationClasses = notificationService.getClasses(notificationService.notifications);
});
};
notificationService.reset = function() {
$interval.cancel(pollTimerHandle);
pollTimerHandle = $interval(notificationService.update, 5 * 60 * 1000 /* five minutes */);
};
// Watch for plan changes and update.
PlanService.registerListener(this, function(plan) {
notificationService.reset();
notificationService.update();
});
// Watch for user changes and update.
$rootScope.$watch(function() { return UserService.currentUser(); }, function(currentUser) {
notificationService.reset();
notificationService.update();
});
return notificationService;
}]);

View file

@ -0,0 +1,8 @@
/**
* Service which provides the OAuth scopes defined.
*/
angular.module('quay').factory('OAuthService', ['$location', 'Config', function($location, Config) {
var oauthService = {};
oauthService.SCOPES = window.__auth_scopes;
return oauthService;
}]);

View file

@ -0,0 +1,99 @@
/**
* Service which pings an endpoint URL and estimates the latency to it.
*/
angular.module('quay').factory('PingService', [function() {
var pingService = {};
var pingCache = {};
var invokeCallback = function($scope, pings, callback) {
if (pings[0] == -1) {
setTimeout(function() {
$scope.$apply(function() {
callback(-1, false, -1);
});
}, 0);
return;
}
var sum = 0;
for (var i = 0; i < pings.length; ++i) {
sum += pings[i];
}
// Report the average ping.
setTimeout(function() {
$scope.$apply(function() {
callback(Math.floor(sum / pings.length), true, pings.length);
});
}, 0);
};
var reportPingResult = function($scope, url, ping, callback) {
// Lookup the cached ping data, if any.
var cached = pingCache[url];
if (!cached) {
cached = pingCache[url] = {
'pings': []
};
}
// If an error occurred, report it and done.
if (ping < 0) {
cached['pings'] = [-1];
invokeCallback($scope, [-1], callback);
return;
}
// Otherwise, add the current ping and determine the average.
cached['pings'].push(ping);
// Invoke the callback.
invokeCallback($scope, cached['pings'], callback);
// Schedule another check if we've done less than three.
if (cached['pings'].length < 3) {
setTimeout(function() {
pingUrlInternal($scope, url, callback);
}, 1000);
}
};
var pingUrlInternal = function($scope, url, callback) {
var path = url + '?cb=' + (Math.random() * 100);
var start = new Date();
var xhr = new XMLHttpRequest();
xhr.onerror = function() {
reportPingResult($scope, url, -1, callback);
};
xhr.onreadystatechange = function () {
if (xhr.readyState === xhr.HEADERS_RECEIVED) {
if (xhr.status != 200) {
reportPingResult($scope, url, -1, callback);
return;
}
var ping = (new Date() - start);
reportPingResult($scope, url, ping, callback);
}
};
xhr.open("GET", path);
xhr.send(null);
};
pingService.pingUrl = function($scope, url, callback) {
if (pingCache[url]) {
invokeCallback($scope, pingCache[url]['pings'], callback);
return;
}
// Note: We do each in a callback after 1s to prevent it running when other code
// runs (which can skew the results).
setTimeout(function() {
pingUrlInternal($scope, url, callback);
}, 1000);
};
return pingService;
}]);

View file

@ -0,0 +1,357 @@
/**
* Helper service for loading, changing and working with subscription plans.
*/
angular.module('quay')
.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService', 'Features', 'Config',
function(KeyService, UserService, CookieService, ApiService, Features, Config) {
var plans = null;
var planDict = {};
var planService = {};
var listeners = [];
var previousSubscribeFailure = false;
planService.getFreePlan = function() {
return 'free';
};
planService.registerListener = function(obj, callback) {
listeners.push({'obj': obj, 'callback': callback});
};
planService.unregisterListener = function(obj) {
for (var i = 0; i < listeners.length; ++i) {
if (listeners[i].obj == obj) {
listeners.splice(i, 1);
break;
}
}
};
planService.notePlan = function(planId) {
if (Features.BILLING) {
CookieService.putSession('quay.notedplan', planId);
}
};
planService.isOrgCompatible = function(plan) {
return plan['stripeId'] == planService.getFreePlan() || plan['bus_features'];
};
planService.getMatchingBusinessPlan = function(callback) {
planService.getPlans(function() {
planService.getSubscription(null, function(sub) {
var plan = planDict[sub.plan];
if (!plan) {
planService.getMinimumPlan(0, true, callback);
return;
}
var count = Math.max(sub.usedPrivateRepos, plan.privateRepos);
planService.getMinimumPlan(count, true, callback);
}, function() {
planService.getMinimumPlan(0, true, callback);
});
});
};
planService.handleNotedPlan = function() {
var planId = planService.getAndResetNotedPlan();
if (!planId || !Features.BILLING) { return false; }
UserService.load(function() {
if (UserService.currentUser().anonymous) {
return;
}
planService.getPlan(planId, function(plan) {
if (planService.isOrgCompatible(plan)) {
document.location = '/organizations/new/?plan=' + planId;
} else {
document.location = '/user?plan=' + planId;
}
});
});
return true;
};
planService.getAndResetNotedPlan = function() {
var planId = CookieService.get('quay.notedplan');
CookieService.clear('quay.notedplan');
return planId;
};
planService.handleCardError = function(resp) {
if (!planService.isCardError(resp)) { return; }
bootbox.dialog({
"message": resp.data.carderror,
"title": "Credit card issue",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
};
planService.isCardError = function(resp) {
return resp && resp.data && resp.data.carderror;
};
planService.verifyLoaded = function(callback) {
if (!Features.BILLING) { return; }
if (plans && plans.length) {
callback(plans);
return;
}
ApiService.listPlans().then(function(data) {
plans = data.plans || [];
for(var i = 0; i < plans.length; i++) {
planDict[plans[i].stripeId] = plans[i];
}
callback(plans);
}, function() { callback([]); });
};
planService.getPlans = function(callback, opt_includePersonal) {
planService.verifyLoaded(function() {
var filtered = [];
for (var i = 0; i < plans.length; ++i) {
var plan = plans[i];
if (plan['deprecated']) { continue; }
if (!opt_includePersonal && !planService.isOrgCompatible(plan)) { continue; }
filtered.push(plan);
}
callback(filtered);
});
};
planService.getPlan = function(planId, callback) {
planService.getPlanIncludingDeprecated(planId, function(plan) {
if (!plan['deprecated']) {
callback(plan);
}
});
};
planService.getPlanIncludingDeprecated = function(planId, callback) {
planService.verifyLoaded(function() {
if (planDict[planId]) {
callback(planDict[planId]);
}
});
};
planService.getMinimumPlan = function(privateCount, isBusiness, callback) {
planService.getPlans(function(plans) {
for (var i = 0; i < plans.length; i++) {
var plan = plans[i];
if (plan.privateRepos >= privateCount) {
callback(plan);
return;
}
}
callback(null);
}, /* include personal */!isBusiness);
};
planService.getSubscription = function(orgname, success, failure) {
if (!Features.BILLING) { return; }
ApiService.getSubscription(orgname).then(success, failure);
};
planService.setSubscription = function(orgname, planId, success, failure, opt_token) {
if (!Features.BILLING) { return; }
var subscriptionDetails = {
plan: planId
};
if (opt_token) {
subscriptionDetails['token'] = opt_token.id;
}
ApiService.updateSubscription(orgname, subscriptionDetails).then(function(resp) {
success(resp);
planService.getPlan(planId, function(plan) {
for (var i = 0; i < listeners.length; ++i) {
listeners[i]['callback'](plan);
}
});
}, failure);
};
planService.getCardInfo = function(orgname, callback) {
if (!Features.BILLING) { return; }
ApiService.getCard(orgname).then(function(resp) {
callback(resp.card);
}, function() {
callback({'is_valid': false});
});
};
planService.changePlan = function($scope, orgname, planId, callbacks, opt_async) {
if (!Features.BILLING) { return; }
if (callbacks['started']) {
callbacks['started']();
}
planService.getPlan(planId, function(plan) {
if (orgname && !planService.isOrgCompatible(plan)) { return; }
planService.getCardInfo(orgname, function(cardInfo) {
if (plan.price > 0 && (previousSubscribeFailure || !cardInfo.last4)) {
var title = cardInfo.last4 ? 'Subscribe' : 'Start Trial ({{amount}} plan)';
planService.showSubscribeDialog($scope, orgname, planId, callbacks, title, /* async */true);
return;
}
previousSubscribeFailure = false;
planService.setSubscription(orgname, planId, callbacks['success'], function(resp) {
previousSubscribeFailure = true;
planService.handleCardError(resp);
callbacks['failure'](resp);
});
});
});
};
planService.changeCreditCard = function($scope, orgname, callbacks) {
if (!Features.BILLING) { return; }
if (callbacks['opening']) {
callbacks['opening']();
}
var submitted = false;
var submitToken = function(token) {
if (submitted) { return; }
submitted = true;
$scope.$apply(function() {
if (callbacks['started']) {
callbacks['started']();
}
var cardInfo = {
'token': token.id
};
ApiService.setCard(orgname, cardInfo).then(callbacks['success'], function(resp) {
planService.handleCardError(resp);
callbacks['failure'](resp);
});
});
};
var email = planService.getEmail(orgname);
StripeCheckout.open({
key: KeyService.stripePublishableKey,
address: false,
email: email,
currency: 'usd',
name: 'Update credit card',
description: 'Enter your credit card number',
panelLabel: 'Update',
token: submitToken,
image: 'static/img/quay-icon-stripe.png',
opened: function() { $scope.$apply(function() { callbacks['opened']() }); },
closed: function() { $scope.$apply(function() { callbacks['closed']() }); }
});
};
planService.getEmail = function(orgname) {
var email = null;
if (UserService.currentUser()) {
email = UserService.currentUser().email;
if (orgname) {
org = UserService.getOrganization(orgname);
if (org) {
emaiil = org.email;
}
}
}
return email;
};
planService.showSubscribeDialog = function($scope, orgname, planId, callbacks, opt_title, opt_async) {
if (!Features.BILLING) { return; }
// If the async parameter is true and this is a browser that does not allow async popup of the
// Stripe dialog (such as Mobile Safari or IE), show a bootbox to show the dialog instead.
var isIE = navigator.appName.indexOf("Internet Explorer") != -1;
var isMobileSafari = navigator.userAgent.match(/(iPod|iPhone|iPad)/) && navigator.userAgent.match(/AppleWebKit/);
if (opt_async && (isIE || isMobileSafari)) {
bootbox.dialog({
"message": "Please click 'Subscribe' to continue",
"buttons": {
"subscribe": {
"label": "Subscribe",
"className": "btn-primary",
"callback": function() {
planService.showSubscribeDialog($scope, orgname, planId, callbacks, opt_title, false);
}
},
"close": {
"label": "Cancel",
"className": "btn-default"
}
}
});
return;
}
if (callbacks['opening']) {
callbacks['opening']();
}
var submitted = false;
var submitToken = function(token) {
if (submitted) { return; }
submitted = true;
if (Config.MIXPANEL_KEY) {
mixpanel.track('plan_subscribe');
}
$scope.$apply(function() {
if (callbacks['started']) {
callbacks['started']();
}
planService.setSubscription(orgname, planId, callbacks['success'], callbacks['failure'], token);
});
};
planService.getPlan(planId, function(planDetails) {
var email = planService.getEmail(orgname);
StripeCheckout.open({
key: KeyService.stripePublishableKey,
address: false,
email: email,
amount: planDetails.price,
currency: 'usd',
name: 'Quay.io ' + planDetails.title + ' Subscription',
description: 'Up to ' + planDetails.privateRepos + ' private repositories',
panelLabel: opt_title || 'Subscribe',
token: submitToken,
image: 'static/img/quay-icon-stripe.png',
opened: function() { $scope.$apply(function() { callbacks['opened']() }); },
closed: function() { $scope.$apply(function() { callbacks['closed']() }); }
});
});
};
return planService;
}]);

View file

@ -0,0 +1,114 @@
/**
* Service for building strings, with wildcards replaced with metadata.
*/
angular.module('quay').factory('StringBuilderService', ['$sce', 'UtilService', function($sce, UtilService) {
var stringBuilderService = {};
stringBuilderService.buildUrl = function(value_or_func, metadata) {
var url = value_or_func;
if (typeof url != 'string') {
url = url(metadata);
}
// Find the variables to be replaced.
var varNames = [];
for (var i = 0; i < url.length; ++i) {
var c = url[i];
if (c == '{') {
for (var j = i + 1; j < url.length; ++j) {
var d = url[j];
if (d == '}') {
varNames.push(url.substring(i + 1, j));
i = j;
break;
}
}
}
}
// Replace all variables found.
for (var i = 0; i < varNames.length; ++i) {
var varName = varNames[i];
if (!metadata[varName]) {
return null;
}
url = url.replace('{' + varName + '}', metadata[varName]);
}
return url;
};
stringBuilderService.buildString = function(value_or_func, metadata) {
var fieldIcons = {
'inviter': 'user',
'username': 'user',
'user': 'user',
'email': 'envelope',
'activating_username': 'user',
'delegate_user': 'user',
'delegate_team': 'group',
'team': 'group',
'token': 'key',
'repo': 'hdd-o',
'robot': 'wrench',
'tag': 'tag',
'role': 'th-large',
'original_role': 'th-large',
'application_name': 'cloud',
'image': 'archive',
'original_image': 'archive',
'client_id': 'chain'
};
var filters = {
'obj': function(value) {
if (!value) { return []; }
return Object.getOwnPropertyNames(value);
},
'updated_tags': function(value) {
if (!value) { return []; }
return Object.getOwnPropertyNames(value);
}
};
var description = value_or_func;
if (typeof description != 'string') {
description = description(metadata);
}
for (var key in metadata) {
if (metadata.hasOwnProperty(key)) {
var value = metadata[key] != null ? metadata[key] : '(Unknown)';
if (filters[key]) {
value = filters[key](value);
}
if (Array.isArray(value)) {
value = value.join(', ');
}
value = value.toString();
if (key.indexOf('image') >= 0) {
value = value.substr(0, 12);
}
var safe = UtilService.escapeHtmlString(value);
var markedDown = UtilService.getMarkedDown(value);
markedDown = markedDown.substr('<p>'.length, markedDown.length - '<p></p>'.length);
var icon = fieldIcons[key];
if (icon) {
markedDown = '<i class="fa fa-' + icon + '"></i>' + markedDown;
}
description = description.replace('{' + key + '}', '<code title="' + safe + '">' + markedDown + '</code>');
}
}
return $sce.trustAsHtml(description.replace('\n', '<br>'));
};
return stringBuilderService;
}]);

View file

@ -0,0 +1,65 @@
/**
* Helper service for defining the various kinds of build triggers and retrieving information
* about them.
*/
angular.module('quay').factory('TriggerService', ['UtilService', '$sanitize', 'KeyService',
function(UtilService, $sanitize, KeyService) {
var triggerService = {};
var triggerTypes = {
'github': {
'description': function(config) {
var source = UtilService.textToSafeHtml(config['build_source']);
var desc = '<i class="fa fa-github fa-lg" style="margin-left: 2px; margin-right: 2px"></i> Push to Github Repository ';
desc += '<a href="https://github.com/' + source + '" target="_blank">' + source + '</a>';
desc += '<br>Dockerfile folder: //' + UtilService.textToSafeHtml(config['subdir']);
return desc;
},
'run_parameters': [
{
'title': 'Branch',
'type': 'option',
'name': 'branch_name'
}
],
'get_redirect_url': function(namespace, repository) {
var redirect_uri = KeyService['githubRedirectUri'] + '/trigger/' +
namespace + '/' + repository;
var authorize_url = KeyService['githubTriggerAuthorizeUrl'];
var client_id = KeyService['githubTriggerClientId'];
return authorize_url + 'client_id=' + client_id +
'&scope=repo,user:email&redirect_uri=' + redirect_uri;
}
}
}
triggerService.getRedirectUrl = function(name, namespace, repository) {
var type = triggerTypes[name];
if (!type) {
return '';
}
return type['get_redirect_url'](namespace, repository);
};
triggerService.getDescription = function(name, config) {
var type = triggerTypes[name];
if (!type) {
return 'Unknown';
}
return type['description'](config);
};
triggerService.getRunParameters = function(name, config) {
var type = triggerTypes[name];
if (!type) {
return [];
}
return type['run_parameters'];
}
return triggerService;
}]);

View file

@ -0,0 +1,37 @@
/**
* Service which provides helper methods for performing some simple UI operations.
*/
angular.module('quay').factory('UIService', [function() {
var uiService = {};
uiService.hidePopover = function(elem) {
var popover = $(elem).data('bs.popover');
if (popover) {
popover.hide();
}
};
uiService.showPopover = function(elem, content) {
var popover = $(elem).data('bs.popover');
if (!popover) {
$(elem).popover({'content': '-', 'placement': 'left'});
}
setTimeout(function() {
var popover = $(elem).data('bs.popover');
popover.options.content = content;
popover.show();
}, 500);
};
uiService.showFormError = function(elem, result) {
var message = result.data['message'] || result.data['error_description'] || '';
if (message) {
uiService.showPopover(elem, message);
} else {
uiService.hidePopover(elem);
}
};
return uiService;
}]);

View file

@ -0,0 +1,131 @@
/**
* Service which monitors the current user session and provides methods for returning information
* about the user.
*/
angular.module('quay')
.factory('UserService', ['ApiService', 'CookieService', '$rootScope', 'Config',
function(ApiService, CookieService, $rootScope, Config) {
var userResponse = {
verified: false,
anonymous: true,
username: null,
email: null,
organizations: [],
logins: []
}
var userService = {}
userService.hasEverLoggedIn = function() {
return CookieService.get('quay.loggedin') == 'true';
};
userService.updateUserIn = function(scope, opt_callback) {
scope.$watch(function () { return userService.currentUser(); }, function (currentUser) {
scope.user = currentUser;
if (opt_callback) {
opt_callback(currentUser);
}
}, true);
};
userService.load = function(opt_callback) {
var handleUserResponse = function(loadedUser) {
userResponse = loadedUser;
if (!userResponse.anonymous) {
if (Config.MIXPANEL_KEY) {
mixpanel.identify(userResponse.username);
mixpanel.people.set({
'$email': userResponse.email,
'$username': userResponse.username,
'verified': userResponse.verified
});
mixpanel.people.set_once({
'$created': new Date()
})
}
if (window.olark !== undefined) {
olark('api.visitor.getDetails', function(details) {
if (details.fullName === null) {
olark('api.visitor.updateFullName', {fullName: userResponse.username});
}
});
olark('api.visitor.updateEmailAddress', {emailAddress: userResponse.email});
olark('api.chat.updateVisitorStatus', {snippet: 'username: ' + userResponse.username});
}
if (window.Raven !== undefined) {
Raven.setUser({
email: userResponse.email,
id: userResponse.username
});
}
CookieService.putPermanent('quay.loggedin', 'true');
} else {
if (window.Raven !== undefined) {
Raven.setUser();
}
}
if (opt_callback) {
opt_callback();
}
};
ApiService.getLoggedInUser().then(function(loadedUser) {
handleUserResponse(loadedUser);
}, function() {
handleUserResponse({'anonymous': true});
});
};
userService.getOrganization = function(name) {
if (!userResponse || !userResponse.organizations) { return null; }
for (var i = 0; i < userResponse.organizations.length; ++i) {
var org = userResponse.organizations[i];
if (org.name == name) {
return org;
}
}
return null;
};
userService.isNamespaceAdmin = function(namespace) {
if (namespace == userResponse.username) {
return true;
}
var org = userService.getOrganization(namespace);
if (!org) {
return false;
}
return org.is_org_admin;
};
userService.isKnownNamespace = function(namespace) {
if (namespace == userResponse.username) {
return true;
}
var org = userService.getOrganization(namespace);
return !!org;
};
userService.currentUser = function() {
return userResponse;
};
// Update the user in the root scope.
userService.updateUserIn($rootScope);
// Load the user the first time.
userService.load();
return userService;
}]);

View file

@ -0,0 +1,88 @@
/**
* Service which exposes various utility methods.
*/
angular.module('quay').factory('UtilService', ['$sanitize', function($sanitize) {
var utilService = {};
utilService.isEmailAddress = function(val) {
var emailRegex = /^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
return emailRegex.test(val);
};
utilService.getMarkedDown = function(string) {
return Markdown.getSanitizingConverter().makeHtml(string || '');
};
utilService.getFirstMarkdownLineAsText = function(commentString) {
if (!commentString) { return ''; }
var lines = commentString.split('\n');
var MARKDOWN_CHARS = {
'#': true,
'-': true,
'>': true,
'`': true
};
for (var i = 0; i < lines.length; ++i) {
// Skip code lines.
if (lines[i].indexOf(' ') == 0) {
continue;
}
// Skip empty lines.
if ($.trim(lines[i]).length == 0) {
continue;
}
// Skip control lines.
if (MARKDOWN_CHARS[$.trim(lines[i])[0]]) {
continue;
}
return utilService.getMarkedDown(lines[i]);
}
return '';
};
utilService.escapeHtmlString = function(text) {
var adjusted = text.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
return adjusted;
};
utilService.getRestUrl = function(args) {
var url = '';
for (var i = 0; i < arguments.length; ++i) {
if (i > 0) {
url += '/';
}
url += encodeURI(arguments[i])
}
return url;
};
utilService.textToSafeHtml = function(text) {
return $sanitize(utilService.escapeHtmlString(text));
};
utilService.clickElement = function(el) {
// From: http://stackoverflow.com/questions/16802795/click-not-working-in-mocha-phantomjs-on-certain-elements
var ev = document.createEvent("MouseEvent");
ev.initMouseEvent(
"click",
true /* bubble */, true /* cancelable */,
window, null,
0, 0, 0, 0, /* coordinates */
false, false, false, false, /* modifier keys */
0 /*left*/, null);
el.dispatchEvent(ev);
};
return utilService;
}]);