var TEAM_PATTERN = '^[a-zA-Z][a-zA-Z0-9]+$'; var ROBOT_PATTERN = '^[a-zA-Z][a-zA-Z0-9]{3,29}$'; $.fn.clipboardCopy = function() { if (zeroClipboardSupported) { (new ZeroClipboard($(this))); return true; } this.hide(); return false; }; var zeroClipboardSupported = true; ZeroClipboard.config({ 'swfPath': 'static/lib/ZeroClipboard.swf' }); ZeroClipboard.on("error", function(e) { zeroClipboardSupported = false; }); ZeroClipboard.on('aftercopy', function(e) { var container = e.target.parentNode.parentNode.parentNode; var message = $(container).find('.clipboard-copied-message')[0]; // Resets the animation. var elem = message; elem.style.display = 'none'; elem.classList.remove('animated'); // Show the notification. setTimeout(function() { elem.style.display = 'inline-block'; elem.classList.add('animated'); }, 10); // Reset the notification. setTimeout(function() { elem.style.display = 'none'; }, 5000); }); function getRestUrl(args) { var url = ''; for (var i = 0; i < arguments.length; ++i) { if (i > 0) { url += '/'; } url += encodeURI(arguments[i]) } return url; } function clickElement(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); } function getFirstTextLine(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 getMarkedDown(lines[i]); } return ''; } function createRobotAccount(ApiService, is_org, orgname, name, callback) { ApiService.createRobot(is_org ? orgname : null, null, {'robot_shortname': name}) .then(callback, ApiService.errorDisplay('Cannot create robot account')); } function createOrganizationTeam(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')); } function getMarkedDown(string) { return Markdown.getSanitizingConverter().makeHtml(string || ''); } quayDependencies = ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment', 'mgcrea.ngStrap', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml', 'ngAnimate']; if (window.__config && window.__config.MIXPANEL_KEY) { quayDependencies.push('angulartics'); quayDependencies.push('angulartics.mixpanel'); } quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoadingBarProvider) { cfpLoadingBarProvider.includeSpinner = false; /** * 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. */ $provide.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; }]); /** * Specialized class for conducting an HTTP poll, while properly preventing multiple calls. */ $provide.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; }]); $provide.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; }]); $provide.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; }]); $provide.factory('UtilService', ['$sanitize', function($sanitize) { var utilService = {}; utilService.escapeHtmlString = function(text) { var adjusted = text.replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); return adjusted; }; utilService.textToSafeHtml = function(text) { return $sanitize(utilService.escapeHtmlString(text)); }; return utilService; }]); $provide.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; }]); $provide.factory('TriggerDescriptionBuilder', ['UtilService', '$sanitize', function(UtilService, $sanitize) { var builderService = {}; builderService.getDescription = function(name, config) { switch (name) { case 'github': var source = UtilService.textToSafeHtml(config['build_source']); var desc = ' Push to Github Repository '; desc += '' + source + ''; desc += '
Dockerfile folder: //' + UtilService.textToSafeHtml(config['subdir']); return desc; default: return 'Unknown'; } }; return builderService; }]); $provide.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 = { 'username': 'user', '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 = getMarkedDown(value); markedDown = markedDown.substr('

'.length, markedDown.length - '

'.length); var icon = fieldIcons[key]; if (icon) { markedDown = '' + markedDown; } description = description.replace('{' + key + '}', '' + markedDown + ''); } } return $sce.trustAsHtml(description.replace('\n', '
')); }; return stringBuilderService; }]); $provide.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; }]); $provide.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; }]); $provide.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; }]); $provide.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) { // We already have /api/v1/ on the URLs, so remove them from the paths. path = path.substr('/api/v1/'.length, path.length); 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); } url += parameters[varName]; i = end; continue; } url += c; } 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 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 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 operation. if it succeeds, then resolve the // deferred promise with the result. Otherwise, reject the same. apiService[opName].apply(apiService, opArgs).then(function(resp) { deferred.resolve(resp); }, function(resp) { deferred.reject(resp); }); }, function(resp) { // Reject with the sign in error. deferred.reject({'data': {'message': 'Invalid verification credentials'}}); }); }; 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:' + '
' + '' + '
', "title": 'Please Verify', "buttons": { "verify": { "label": "Verify", "className": "btn-success", "callback": verifyNow }, "close": { "label": "Cancel", "className": "btn-default", "callback": function() { deferred.reject({'data': {'message': '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) { var one = Restangular.one(buildUrl(path, opt_parameters)); 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; }]); $provide.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; }]); $provide.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; }]); $provide.factory('ExternalNotificationData', ['Config', function(Config) { var externalNotificationData = {}; var events = [ { 'id': 'repo_push', 'title': 'Push to Repository', 'icon': 'fa-upload' }, { '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' } ]; 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' } ] }, { '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': 'Notification Token' } ] }, { 'id': 'slack', 'title': 'Slack Room Notification', 'icon': 'slack-icon', 'fields': [ { 'name': 'subdomain', 'type': 'string', 'title': 'Slack Subdomain' }, { 'name': 'token', 'type': 'string', 'title': 'Token', 'help_url': 'https://{subdomain}.slack.com/services/new/incoming-webhook' } ] } ]; 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() { return methods; }; externalNotificationData.getEventInfo = function(event) { return eventMap[event]; }; externalNotificationData.getMethodInfo = function(method) { return methodMap[method]; }; return externalNotificationData; }]); $provide.factory('NotificationService', ['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', 'PlanService', 'UserService', 'Config', function($rootScope, $interval, UserService, ApiService, StringBuilderService, PlanService, UserService, Config) { 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 }, '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. ' + '

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} ' + '

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} has 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.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 (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; }]); $provide.factory('KeyService', ['$location', 'Config', function($location, Config) { var keyService = {} keyService['stripePublishableKey'] = Config['STRIPE_PUBLISHABLE_KEY']; keyService['githubClientId'] = Config['GITHUB_CLIENT_ID']; keyService['githubLoginClientId'] = Config['GITHUB_LOGIN_CLIENT_ID']; keyService['githubRedirectUri'] = Config.getUrl('/oauth2/github/callback'); keyService['googleLoginClientId'] = Config['GOOGLE_LOGIN_CLIENT_ID']; keyService['googleRedirectUri'] = Config.getUrl('/oauth2/google/callback'); keyService['googleLoginUrl'] = 'https://accounts.google.com/o/oauth2/auth?response_type=code&'; keyService['githubLoginUrl'] = 'https://github.com/login/oauth/authorize?'; keyService['googleLoginScope'] = 'openid email'; keyService['githubLoginScope'] = 'user:email'; 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; }]); $provide.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; }]); }). directive('match', function($parse) { return { require: 'ngModel', link: function(scope, elem, attrs, ctrl) { scope.$watch(function() { return $parse(attrs.match)(scope) === ctrl.$modelValue; }, function(currentValue) { ctrl.$setValidity('mismatch', currentValue); }); } }; }). directive('onresize', function ($window, $parse) { return function (scope, element, attr) { var fn = $parse(attr.onresize); var notifyResized = function() { scope.$apply(function () { fn(scope); }); }; angular.element($window).on('resize', null, notifyResized); scope.$on('$destroy', function() { angular.element($window).off('resize', null, notifyResized); }); }; }). config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) { var title = window.__config['REGISTRY_TITLE'] || 'Quay.io'; $locationProvider.html5Mode(true); // WARNING WARNING WARNING // If you add a route here, you must add a corresponding route in thr endpoints/web.py // index rule to make sure that deep links directly deep into the app continue to work. // WARNING WARNING WARNING $routeProvider. when('/repository/:namespace/:name', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl, fixFooter: false, reloadOnSearch: false}). when('/repository/:namespace/:name/tag/:tag', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl, fixFooter: false}). when('/repository/:namespace/:name/image/:image', {templateUrl: '/static/partials/image-view.html', controller: ImageViewCtrl, reloadOnSearch: false}). when('/repository/:namespace/:name/admin', {templateUrl: '/static/partials/repo-admin.html', controller:RepoAdminCtrl, reloadOnSearch: false}). when('/repository/:namespace/:name/build', {templateUrl: '/static/partials/repo-build.html', controller:RepoBuildCtrl, reloadOnSearch: false}). when('/repository/:namespace/:name/build/:buildid/buildpack', {templateUrl: '/static/partials/build-package.html', controller:BuildPackageCtrl, reloadOnSearch: false}). when('/repository/', {title: 'Repositories', description: 'Public and private docker repositories list', templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl, reloadOnSearch: false}). when('/user/', {title: 'Account Settings', description:'Account settings for ' + title, templateUrl: '/static/partials/user-admin.html', reloadOnSearch: false, controller: UserAdminCtrl}). when('/superuser/', {title: 'Superuser Admin Panel', description:'Admin panel for ' + title, templateUrl: '/static/partials/super-user.html', reloadOnSearch: false, controller: SuperUserAdminCtrl}). when('/guide/', {title: 'Guide', description:'Guide to using private docker repositories on ' + title, templateUrl: '/static/partials/guide.html', controller: GuideCtrl}). when('/tutorial/', {title: 'Tutorial', description:'Interactive tutorial for using ' + title, templateUrl: '/static/partials/tutorial.html', controller: TutorialCtrl}). when('/contact/', {title: 'Contact Us', description:'Different ways for you to get a hold of us when you need us most.', templateUrl: '/static/partials/contact.html', controller: ContactCtrl}). when('/about/', {title: 'About Us', description:'Information about the Quay.io team and the company.', templateUrl: '/static/partials/about.html'}). when('/plans/', {title: 'Plans and Pricing', description: 'Plans and pricing for private docker repositories on Quay.io', templateUrl: '/static/partials/plans.html', controller: PlansCtrl}). when('/security/', {title: 'Security', description: 'Security features used when transmitting and storing data', templateUrl: '/static/partials/security.html'}). when('/signin/', {title: 'Sign In', description: 'Sign into ' + title, templateUrl: '/static/partials/signin.html'}). when('/new/', {title: 'Create new repository', description: 'Create a new public or private docker repository, optionally constructing from a dockerfile', templateUrl: '/static/partials/new-repo.html', controller: NewRepoCtrl}). when('/organizations/', {title: 'Organizations', description: 'Private docker repository hosting for businesses and organizations', templateUrl: '/static/partials/organizations.html', controller: OrgsCtrl}). when('/organizations/new/', {title: 'New Organization', description: 'Create a new organization on ' + title, templateUrl: '/static/partials/new-organization.html', controller: NewOrgCtrl}). when('/organization/:orgname', {templateUrl: '/static/partials/org-view.html', controller: OrgViewCtrl}). when('/organization/:orgname/admin', {templateUrl: '/static/partials/org-admin.html', controller: OrgAdminCtrl, reloadOnSearch: false}). when('/organization/:orgname/teams/:teamname', {templateUrl: '/static/partials/team-view.html', controller: TeamViewCtrl}). when('/organization/:orgname/logs/:membername', {templateUrl: '/static/partials/org-member-logs.html', controller: OrgMemberLogsCtrl}). when('/organization/:orgname/application/:clientid', {templateUrl: '/static/partials/manage-application.html', controller: ManageApplicationCtrl, reloadOnSearch: false}). when('/v1/', {title: 'Activation information', templateUrl: '/static/partials/v1-page.html', controller: V1Ctrl}). when('/tour/', {title: title + ' Tour', templateUrl: '/static/partials/tour.html', controller: TourCtrl}). when('/tour/organizations', {title: 'Teams and Organizations Tour', templateUrl: '/static/partials/tour.html', controller: TourCtrl}). when('/tour/features', {title: title + ' Features', templateUrl: '/static/partials/tour.html', controller: TourCtrl}). when('/tour/enterprise', {title: 'Enterprise Edition', templateUrl: '/static/partials/tour.html', controller: TourCtrl}). when('/', {title: 'Hosted Private Docker Registry', templateUrl: '/static/partials/landing.html', controller: LandingCtrl, pageClass: 'landing-page'}). otherwise({redirectTo: '/'}); }]). config(function(RestangularProvider) { RestangularProvider.setBaseUrl('/api/v1/'); }); if (window.__config && window.__config.MIXPANEL_KEY) { quayApp.config(['$analyticsProvider', function($analyticsProvider) { $analyticsProvider.virtualPageviews(true); }]); } if (window.__config && window.__config.SENTRY_PUBLIC_DSN) { quayApp.config(function($provide) { $provide.decorator("$exceptionHandler", function($delegate) { return function(ex, cause) { $delegate(ex, cause); Raven.captureException(ex, {extra: {cause: cause}}); }; }); }); } function buildConditionalLinker($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; } } }); } } quayApp.directive('quayRequire', function ($animate, Features) { return { transclude: 'element', priority: 600, terminal: true, restrict: 'A', link: buildConditionalLinker($animate, 'quayRequire', function(value) { return Features.matchesFeatures(value); }) }; }); quayApp.directive('quayShow', function($animate, Features, Config) { return { priority: 590, restrict: 'A', link: function($scope, $element, $attr, ctrl, $transclude) { $scope.Features = Features; $scope.Config = Config; $scope.$watch($attr.quayShow, function(result) { $animate[!!result ? 'removeClass' : 'addClass']($element, 'ng-hide'); }); } }; }); quayApp.directive('quaySection', function($animate, $location, $rootScope) { return { priority: 590, restrict: 'A', link: function($scope, $element, $attr, ctrl, $transclude) { var update = function() { var result = $location.path().indexOf('/' + $attr.quaySection) == 0; $animate[!result ? 'removeClass' : 'addClass']($element, 'active'); }; $scope.$watch(function(){ return $location.path(); }, update); $scope.$watch($attr.quaySection, update); } }; }); quayApp.directive('quayClasses', function(Features, Config) { return { priority: 580, restrict: 'A', link: function($scope, $element, $attr, ctrl, $transclude) { // Borrowed from ngClass. function flattenClasses(classVal) { if(angular.isArray(classVal)) { return classVal.join(' '); } else if (angular.isObject(classVal)) { var classes = [], i = 0; angular.forEach(classVal, function(v, k) { if (v) { classes.push(k); } }); return classes.join(' '); } return classVal; } function removeClass(classVal) { $attr.$removeClass(flattenClasses(classVal)); } function addClass(classVal) { $attr.$addClass(flattenClasses(classVal)); } $scope.$watch($attr.quayClasses, function(result) { var scopeVals = { 'Features': Features, 'Config': Config }; for (var expr in result) { if (!result.hasOwnProperty(expr)) { continue; } // Evaluate the expression with the entire features list added. var value = $scope.$eval(expr, scopeVals); if (value) { addClass(result[expr]); } else { removeClass(result[expr]); } } }); } }; }); quayApp.directive('quayInclude', function($compile, $templateCache, $http, Features, Config) { return { priority: 595, restrict: 'A', link: function($scope, $element, $attr, ctrl) { var getTemplate = function(templateName) { var templateUrl = '/static/partials/' + templateName; return $http.get(templateUrl, {cache: $templateCache}); }; var result = $scope.$eval($attr.quayInclude); if (!result) { return; } var scopeVals = { 'Features': Features, 'Config': Config }; var templatePath = null; for (var expr in result) { if (!result.hasOwnProperty(expr)) { continue; } // Evaluate the expression with the entire features list added. var value = $scope.$eval(expr, scopeVals); if (value) { templatePath = result[expr]; break; } } if (!templatePath) { return; } var promise = getTemplate(templatePath).success(function(html) { $element.html(html); }).then(function (response) { $element.replaceWith($compile($element.html())($scope)); if ($attr.onload) { $scope.$eval($attr.onload); } }); } }; }); quayApp.directive('entityReference', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/entity-reference.html', replace: false, transclude: false, restrict: 'C', scope: { 'entity': '=entity', 'namespace': '=namespace' }, controller: function($scope, $element, UserService, UtilService) { $scope.getIsAdmin = function(namespace) { return UserService.isNamespaceAdmin(namespace); }; $scope.getRobotUrl = function(name) { var namespace = $scope.getPrefix(name); if (!namespace) { return ''; } if (!$scope.getIsAdmin(namespace)) { return ''; } var org = UserService.getOrganization(namespace); if (!org) { // This robot is owned by the user. return '/user/?tab=robots&showRobot=' + UtilService.textToSafeHtml(name); } return '/organization/' + org['name'] + '/admin?tab=robots&showRobot=' + UtilService.textToSafeHtml(name); }; $scope.getPrefix = function(name) { if (!name) { return ''; } var plus = name.indexOf('+'); return name.substr(0, plus); }; $scope.getShortenedName = function(name) { if (!name) { return ''; } var plus = name.indexOf('+'); return name.substr(plus + 1); }; } }; return directiveDefinitionObject; }); quayApp.directive('applicationInfo', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/application-info.html', replace: false, transclude: false, restrict: 'C', scope: { 'application': '=application' }, controller: function($scope, $element, ApiService) { } }; return directiveDefinitionObject; }); quayApp.directive('applicationReference', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/application-reference.html', replace: false, transclude: false, restrict: 'C', scope: { 'title': '=title', 'clientId': '=clientId' }, controller: function($scope, $element, ApiService, $modal) { $scope.showAppDetails = function() { var params = { 'client_id': $scope.clientId }; ApiService.getApplicationInformation(null, params).then(function(resp) { $scope.applicationInfo = resp; $modal({ title: 'Application Information', scope: $scope, template: '/static/directives/application-reference-dialog.html', show: true }); }, ApiService.errorDisplay('Application could not be found')); }; } }; return directiveDefinitionObject; }); quayApp.directive('markdownView', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/markdown-view.html', replace: false, transclude: false, restrict: 'C', scope: { 'content': '=content', 'firstLineOnly': '=firstLineOnly' }, controller: function($scope, $element, $sce) { $scope.getMarkedDown = function(content, firstLineOnly) { if (firstLineOnly) { content = getFirstTextLine(content); } return $sce.trustAsHtml(getMarkedDown(content)); }; } }; return directiveDefinitionObject; }); quayApp.directive('repoBreadcrumb', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/repo-breadcrumb.html', replace: false, transclude: false, restrict: 'C', scope: { 'repo': '=repo', 'image': '=image', 'subsection': '=subsection', 'subsectionIcon': '=subsectionIcon' }, controller: function($scope, $element) { } }; return directiveDefinitionObject; }); quayApp.directive('repoCircle', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/repo-circle.html', replace: false, transclude: false, restrict: 'C', scope: { 'repo': '=repo' }, controller: function($scope, $element) { } }; return directiveDefinitionObject; }); quayApp.directive('copyBox', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/copy-box.html', replace: false, transclude: false, restrict: 'C', scope: { 'value': '=value', 'hoveringMessage': '=hoveringMessage', }, controller: function($scope, $element, $rootScope) { $scope.disabled = false; var number = $rootScope.__copyBoxIdCounter || 0; $rootScope.__copyBoxIdCounter = number + 1; $scope.inputId = "copy-box-input-" + number; var button = $($element).find('.copy-icon'); var input = $($element).find('input'); input.attr('id', $scope.inputId); button.attr('data-clipboard-target', $scope.inputId); $scope.disabled = !button.clipboardCopy(); } }; return directiveDefinitionObject; }); quayApp.directive('userSetup', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/user-setup.html', replace: false, transclude: true, restrict: 'C', scope: { 'redirectUrl': '=redirectUrl', 'signInStarted': '&signInStarted', 'signedIn': '&signedIn' }, controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) { $scope.sendRecovery = function() { ApiService.requestRecoveryEmail($scope.recovery).then(function() { $scope.invalidRecovery = false; $scope.errorMessage = ''; $scope.sent = true; }, function(result) { $scope.invalidRecovery = true; $scope.errorMessage = result.data; $scope.sent = false; }); }; $scope.hasSignedIn = function() { return UserService.hasEverLoggedIn(); }; } }; return directiveDefinitionObject; }); quayApp.directive('externalLoginButton', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/external-login-button.html', replace: false, transclude: true, restrict: 'C', scope: { 'signInStarted': '&signInStarted', 'redirectUrl': '=redirectUrl', 'provider': '@provider', 'action': '@action' }, controller: function($scope, $timeout, $interval, ApiService, KeyService, CookieService, Features, Config) { $scope.startSignin = function(service) { $scope.signInStarted({'service': service}); var url = KeyService.getExternalLoginUrl(service, $scope.action || 'login'); // Save the redirect URL in a cookie so that we can redirect back after the service returns to us. var redirectURL = $scope.redirectUrl || window.location.toString(); CookieService.putPermanent('quay.redirectAfterLoad', redirectURL); // Needed to ensure that UI work done by the started callback is finished before the location // changes. $timeout(function() { document.location = url; }, 250); }; } }; return directiveDefinitionObject; }); quayApp.directive('signinForm', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/signin-form.html', replace: false, transclude: true, restrict: 'C', scope: { 'redirectUrl': '=redirectUrl', 'signInStarted': '&signInStarted', 'signedIn': '&signedIn' }, controller: function($scope, $location, $timeout, $interval, ApiService, KeyService, UserService, CookieService, Features, Config) { $scope.tryAgainSoon = 0; $scope.tryAgainInterval = null; $scope.markStarted = function() { if ($scope.signInStarted != null) { $scope.signInStarted(); } }; $scope.cancelInterval = function() { $scope.tryAgainSoon = 0; if ($scope.tryAgainInterval) { $interval.cancel($scope.tryAgainInterval); } $scope.tryAgainInterval = null; }; $scope.$watch('user.username', function() { $scope.cancelInterval(); }); $scope.$on('$destroy', function() { $scope.cancelInterval(); }); $scope.signin = function() { if ($scope.tryAgainSoon > 0) { return; } $scope.markStarted(); $scope.cancelInterval(); ApiService.signinUser($scope.user).then(function() { $scope.needsEmailVerification = false; $scope.invalidCredentials = false; if ($scope.signedIn != null) { $scope.signedIn(); } UserService.load(); // Redirect to the specified page or the landing page // Note: The timeout of 500ms is needed to ensure dialogs containing sign in // forms get removed before the location changes. $timeout(function() { if ($scope.redirectUrl == $location.path()) { return; } $location.path($scope.redirectUrl ? $scope.redirectUrl : '/'); }, 500); }, function(result) { if (result.status == 429 /* try again later */) { $scope.needsEmailVerification = false; $scope.invalidCredentials = false; $scope.cancelInterval(); $scope.tryAgainSoon = result.headers('Retry-After'); $scope.tryAgainInterval = $interval(function() { $scope.tryAgainSoon--; if ($scope.tryAgainSoon <= 0) { $scope.cancelInterval(); } }, 1000, $scope.tryAgainSoon); } else { $scope.needsEmailVerification = result.data.needsEmailVerification; $scope.invalidCredentials = result.data.invalidCredentials; } }); }; } }; return directiveDefinitionObject; }); quayApp.directive('signupForm', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/signup-form.html', replace: false, transclude: true, restrict: 'C', scope: { }, controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, Config, UIService) { $('.form-signup').popover(); $scope.awaitingConfirmation = false; $scope.registering = false; $scope.register = function() { UIService.hidePopover('#signupButton'); $scope.registering = true; ApiService.createNewUser($scope.newUser).then(function() { $scope.registering = false; $scope.awaitingConfirmation = true; if (Config.MIXPANEL_KEY) { mixpanel.alias($scope.newUser.username); } }, function(result) { $scope.registering = false; UIService.showFormError('#signupButton', result); }); }; } }; return directiveDefinitionObject; }); quayApp.directive('tourContent', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/tour-content.html', replace: false, transclude: false, restrict: 'C', scope: { 'kind': '=kind' }, controller: function($scope, $element, $timeout, UserService) { // Monitor any user changes and place the current user into the scope. UserService.updateUserIn($scope); $scope.chromify = function() { browserchrome.update(); }; $scope.$watch('kind', function(kind) { $timeout(function() { $scope.chromify(); }); }); }, link: function($scope, $element, $attr, ctrl) { $scope.chromify(); } }; return directiveDefinitionObject; }); quayApp.directive('plansTable', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/plans-table.html', replace: false, transclude: false, restrict: 'C', scope: { 'plans': '=plans', 'currentPlan': '=currentPlan' }, controller: function($scope, $element) { $scope.setPlan = function(plan) { $scope.currentPlan = plan; }; } }; return directiveDefinitionObject; }); quayApp.directive('dockerAuthDialog', function (Config) { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/docker-auth-dialog.html', replace: false, transclude: true, restrict: 'C', scope: { 'username': '=username', 'token': '=token', 'shown': '=shown', 'counter': '=counter', 'supportsRegenerate': '@supportsRegenerate', 'regenerate': '®enerate' }, controller: function($scope, $element) { var updateCommand = function() { var escape = function(v) { if (!v) { return v; } return v.replace('$', '\\$'); }; $scope.command = 'docker login -e="." -u="' + escape($scope.username) + '" -p="' + $scope.token + '" ' + Config['SERVER_HOSTNAME']; }; $scope.$watch('username', updateCommand); $scope.$watch('token', updateCommand); $scope.regenerating = true; $scope.askRegenerate = function() { bootbox.confirm('Are you sure you want to regenerate the token? All existing login credentials will become invalid', function(resp) { if (resp) { $scope.regenerating = true; $scope.regenerate({'username': $scope.username, 'token': $scope.token}); } }); }; $scope.isDownloadSupported = function() { var isSafari = /^((?!chrome).)*safari/i.test(navigator.userAgent); if (isSafari) { // Doesn't work properly in Safari, sadly. return false; } try { return !!new Blob(); } catch(e) {} return false; }; $scope.downloadCfg = function() { var auth = $.base64.encode($scope.username + ":" + $scope.token); config = {} config[Config.getUrl('/v1/')] = { "auth": auth, "email": "" }; var file = JSON.stringify(config, null, ' '); var blob = new Blob([file]); saveAs(blob, '.dockercfg'); }; var show = function(r) { $scope.regenerating = false; if (!$scope.shown || !$scope.username || !$scope.token) { $('#dockerauthmodal').modal('hide'); return; } $('#copyClipboard').clipboardCopy(); $('#dockerauthmodal').modal({}); }; $scope.$watch('counter', show); $scope.$watch('shown', show); $scope.$watch('username', show); $scope.$watch('token', show); } }; return directiveDefinitionObject; }); quayApp.filter('reverse', function() { return function(items) { return items.slice().reverse(); }; }); quayApp.filter('bytes', function() { return function(bytes, precision) { if (!bytes || isNaN(parseFloat(bytes)) || !isFinite(bytes)) return 'Unknown'; if (typeof precision === 'undefined') precision = 1; var units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'], number = Math.floor(Math.log(bytes) / Math.log(1024)); return (bytes / Math.pow(1024, Math.floor(number))).toFixed(precision) + ' ' + units[number]; } }); quayApp.filter('visibleLogFilter', function () { return function (logs, allowed) { if (!allowed) { return logs; } var filtered = []; angular.forEach(logs, function (log) { if (allowed[log.kind]) { filtered.push(log); } }); return filtered; }; }); quayApp.directive('billingInvoices', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/billing-invoices.html', replace: false, transclude: false, restrict: 'C', scope: { 'organization': '=organization', 'user': '=user', 'makevisible': '=makevisible' }, 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; var hasValidOrg = !!$scope.organization; var isValid = hasValidUser || hasValidOrg; if (!$scope.makevisible || !isValid) { return; } $scope.loading = true; ApiService.listInvoices($scope.organization).then(function(resp) { $scope.invoices = resp.invoices; $scope.loading = false; }); }; $scope.$watch('organization', update); $scope.$watch('user', update); $scope.$watch('makevisible', update); } }; return directiveDefinitionObject; }); quayApp.directive('logsView', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/logs-view.html', replace: false, transclude: false, restrict: 'C', scope: { 'organization': '=organization', 'user': '=user', 'makevisible': '=makevisible', 'repository': '=repository', 'performer': '=performer' }, controller: function($scope, $element, $sce, Restangular, ApiService, TriggerDescriptionBuilder, StringBuilderService, ExternalNotificationData) { $scope.loading = true; $scope.logs = null; $scope.kindsAllowed = null; $scope.chartVisible = true; $scope.logsPath = ''; var datetime = new Date(); $scope.logStartDate = new Date(datetime.getUTCFullYear(), datetime.getUTCMonth(), datetime.getUTCDate() - 7); $scope.logEndDate = new Date(datetime.getUTCFullYear(), datetime.getUTCMonth(), datetime.getUTCDate()); var defaultPermSuffix = function(metadata) { if (metadata.activating_username) { return ', when creating user is {activating_username}'; } return ''; }; var logDescriptions = { 'account_change_plan': 'Change plan', 'account_change_cc': 'Update credit card', 'account_change_password': 'Change password', 'account_convert': 'Convert account to organization', 'create_robot': 'Create Robot Account: {robot}', 'delete_robot': 'Delete Robot Account: {robot}', 'create_repo': 'Create Repository: {repo}', 'push_repo': 'Push to repository: {repo}', 'pull_repo': function(metadata) { if (metadata.token) { return 'Pull repository {repo} via token {token}'; } else if (metadata.username) { return 'Pull repository {repo} by {username}'; } else { return 'Public pull of repository {repo} by {_ip}'; } }, 'delete_repo': 'Delete repository: {repo}', 'change_repo_permission': function(metadata) { if (metadata.username) { return 'Change permission for user {username} in repository {repo} to {role}'; } else if (metadata.team) { return 'Change permission for team {team} in repository {repo} to {role}'; } else if (metadata.token) { return 'Change permission for token {token} in repository {repo} to {role}'; } }, 'delete_repo_permission': function(metadata) { if (metadata.username) { return 'Remove permission for user {username} from repository {repo}'; } else if (metadata.team) { return 'Remove permission for team {team} from repository {repo}'; } else if (metadata.token) { return 'Remove permission for token {token} from repository {repo}'; } }, 'delete_tag': 'Tag {tag} deleted in repository {repo} by user {username}', 'create_tag': 'Tag {tag} created in repository {repo} on image {image} by user {username}', 'move_tag': 'Tag {tag} moved from image {original_image} to image {image} in repository {repo} by user {username}', 'change_repo_visibility': 'Change visibility for repository {repo} to {visibility}', 'add_repo_accesstoken': 'Create access token {token} in repository {repo}', 'delete_repo_accesstoken': 'Delete access token {token} in repository {repo}', 'set_repo_description': 'Change description for repository {repo}: {description}', 'build_dockerfile': function(metadata) { if (metadata.trigger_id) { var triggerDescription = TriggerDescriptionBuilder.getDescription( metadata['service'], metadata['config']); return 'Build image from Dockerfile for repository {repo} triggered by ' + triggerDescription; } return 'Build image from Dockerfile for repository {repo}'; }, 'org_create_team': 'Create team: {team}', 'org_delete_team': 'Delete team: {team}', 'org_add_team_member': 'Add member {member} to team {team}', 'org_remove_team_member': 'Remove member {member} from team {team}', 'org_set_team_description': 'Change description of team {team}: {description}', 'org_set_team_role': 'Change permission of team {team} to {role}', 'create_prototype_permission': function(metadata) { if (metadata.delegate_user) { return 'Create default permission: {role} for {delegate_user}' + defaultPermSuffix(metadata); } else if (metadata.delegate_team) { return 'Create default permission: {role} for {delegate_team}' + defaultPermSuffix(metadata); } }, 'modify_prototype_permission': function(metadata) { if (metadata.delegate_user) { return 'Modify default permission: {role} (from {original_role}) for {delegate_user}' + defaultPermSuffix(metadata); } else if (metadata.delegate_team) { return 'Modify default permission: {role} (from {original_role}) for {delegate_team}' + defaultPermSuffix(metadata); } }, 'delete_prototype_permission': function(metadata) { if (metadata.delegate_user) { return 'Delete default permission: {role} for {delegate_user}' + defaultPermSuffix(metadata); } else if (metadata.delegate_team) { return 'Delete default permission: {role} for {delegate_team}' + defaultPermSuffix(metadata); } }, 'setup_repo_trigger': function(metadata) { var triggerDescription = TriggerDescriptionBuilder.getDescription( metadata['service'], metadata['config']); return 'Setup build trigger - ' + triggerDescription; }, 'delete_repo_trigger': function(metadata) { var triggerDescription = TriggerDescriptionBuilder.getDescription( metadata['service'], metadata['config']); return 'Delete build trigger - ' + triggerDescription; }, 'create_application': 'Create application {application_name} with client ID {client_id}', 'update_application': 'Update application to {application_name} for client ID {client_id}', 'delete_application': 'Delete application {application_name} with client ID {client_id}', 'reset_application_client_secret': 'Reset the Client Secret of application {application_name} ' + 'with client ID {client_id}', 'add_repo_notification': function(metadata) { var eventData = ExternalNotificationData.getEventInfo(metadata.event); return 'Add notification of event "' + eventData['title'] + '" for repository {repo}'; }, 'delete_repo_notification': function(metadata) { var eventData = ExternalNotificationData.getEventInfo(metadata.event); return 'Delete notification of event "' + eventData['title'] + '" for repository {repo}'; }, 'regenerate_robot_token': 'Regenerated token for robot {robot}', // Note: These are deprecated. 'add_repo_webhook': 'Add webhook in repository {repo}', 'delete_repo_webhook': 'Delete webhook in repository {repo}' }; var logKinds = { 'account_change_plan': 'Change plan', 'account_change_cc': 'Update credit card', 'account_change_password': 'Change password', 'account_convert': 'Convert account to organization', 'create_robot': 'Create Robot Account', 'delete_robot': 'Delete Robot Account', 'create_repo': 'Create Repository', 'push_repo': 'Push to repository', 'pull_repo': 'Pull repository', 'delete_repo': 'Delete repository', 'change_repo_permission': 'Change repository permission', 'delete_repo_permission': 'Remove user permission from repository', 'change_repo_visibility': 'Change repository visibility', 'add_repo_accesstoken': 'Create access token', 'delete_repo_accesstoken': 'Delete access token', 'set_repo_description': 'Change repository description', 'build_dockerfile': 'Build image from Dockerfile', 'delete_tag': 'Delete Tag', 'create_tag': 'Create Tag', 'move_tag': 'Move Tag', 'org_create_team': 'Create team', 'org_delete_team': 'Delete team', 'org_add_team_member': 'Add team member', 'org_remove_team_member': 'Remove team member', 'org_set_team_description': 'Change team description', 'org_set_team_role': 'Change team permission', 'create_prototype_permission': 'Create default permission', 'modify_prototype_permission': 'Modify default permission', 'delete_prototype_permission': 'Delete default permission', 'setup_repo_trigger': 'Setup build trigger', 'delete_repo_trigger': 'Delete build trigger', 'create_application': 'Create Application', 'update_application': 'Update Application', 'delete_application': 'Delete Application', 'reset_application_client_secret': 'Reset Client Secret', 'add_repo_notification': 'Add repository notification', 'delete_repo_notification': 'Delete repository notification', 'regenerate_robot_token': 'Regenerate Robot Token', // Note: these are deprecated. 'add_repo_webhook': 'Add webhook', 'delete_repo_webhook': 'Delete webhook' }; var getDateString = function(date) { return (date.getMonth() + 1) + '/' + date.getDate() + '/' + date.getFullYear(); }; var getOffsetDate = function(date, days) { return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days); }; var update = function() { var hasValidUser = !!$scope.user; var hasValidOrg = !!$scope.organization; var hasValidRepo = $scope.repository && $scope.repository.namespace; var isValid = hasValidUser || hasValidOrg || hasValidRepo; if (!$scope.makevisible || !isValid) { return; } var twoWeeksAgo = getOffsetDate($scope.logEndDate, -14); if ($scope.logStartDate > $scope.logEndDate || $scope.logStartDate < twoWeeksAgo) { $scope.logStartDate = twoWeeksAgo; } $scope.loading = true; // Note: We construct the URLs here manually because we also use it for the download // path. var url = getRestUrl('user/logs'); if ($scope.organization) { url = getRestUrl('organization', $scope.organization.name, 'logs'); } if ($scope.repository) { url = getRestUrl('repository', $scope.repository.namespace, $scope.repository.name, 'logs'); } url += '?starttime=' + encodeURIComponent(getDateString($scope.logStartDate)); url += '&endtime=' + encodeURIComponent(getDateString($scope.logEndDate)); if ($scope.performer) { url += '&performer=' + encodeURIComponent($scope.performer.username); } var loadLogs = Restangular.one(url); loadLogs.customGET().then(function(resp) { $scope.logsPath = '/api/v1/' + url; if (!$scope.chart) { window.console.log('creating chart'); $scope.chart = new LogUsageChart(logKinds); $($scope.chart).bind('filteringChanged', function(e) { $scope.$apply(function() { $scope.kindsAllowed = e.allowed; }); }); } $scope.chart.draw('bar-chart', resp.logs, $scope.logStartDate, $scope.logEndDate); $scope.kindsAllowed = null; $scope.logs = resp.logs; $scope.loading = false; }); }; $scope.toggleChart = function() { $scope.chartVisible = !$scope.chartVisible; }; $scope.isVisible = function(allowed, kind) { return allowed == null || allowed.hasOwnProperty(kind); }; $scope.getColor = function(kind) { return $scope.chart.getColor(kind); }; $scope.getDescription = function(log) { log.metadata['_ip'] = log.ip ? log.ip : null; return StringBuilderService.buildString(logDescriptions[log.kind] || log.kind, log.metadata); }; $scope.$watch('organization', update); $scope.$watch('user', update); $scope.$watch('repository', update); $scope.$watch('makevisible', update); $scope.$watch('performer', update); $scope.$watch('logStartDate', update); $scope.$watch('logEndDate', update); } }; return directiveDefinitionObject; }); quayApp.directive('applicationManager', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/application-manager.html', replace: false, transclude: false, restrict: 'C', scope: { 'organization': '=organization', 'makevisible': '=makevisible' }, controller: function($scope, $element, ApiService) { $scope.loading = false; $scope.applications = []; $scope.createApplication = function(appName) { if (!appName) { return; } var params = { 'orgname': $scope.organization.name }; var data = { 'name': appName }; ApiService.createOrganizationApplication(data, params).then(function(resp) { $scope.applications.push(resp); }, ApiService.errorDisplay('Cannot create application')); }; var update = function() { if (!$scope.organization || !$scope.makevisible) { return; } if ($scope.loading) { return; } $scope.loading = true; var params = { 'orgname': $scope.organization.name }; ApiService.getOrganizationApplications(null, params).then(function(resp) { $scope.loading = false; $scope.applications = resp['applications'] || []; }); }; $scope.$watch('organization', update); $scope.$watch('makevisible', update); } }; return directiveDefinitionObject; }); quayApp.directive('robotsManager', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/robots-manager.html', replace: false, transclude: false, restrict: 'C', scope: { 'organization': '=organization', 'user': '=user' }, controller: function($scope, $element, ApiService, $routeParams) { $scope.ROBOT_PATTERN = ROBOT_PATTERN; $scope.robots = null; $scope.loading = false; $scope.shownRobot = null; $scope.showRobotCounter = 0; $scope.regenerateToken = function(username) { if (!username) { return; } var shortName = $scope.getShortenedName(username); ApiService.regenerateRobotToken($scope.organization, null, {'robot_shortname': shortName}).then(function(updated) { var index = $scope.findRobotIndexByName(username); if (index >= 0) { $scope.robots.splice(index, 1); $scope.robots.push(updated); } $scope.shownRobot = updated; }, ApiService.errorDisplay('Cannot regenerate robot account token')); }; $scope.showRobot = function(info) { $scope.shownRobot = info; $scope.showRobotCounter++; }; $scope.findRobotIndexByName = function(name) { for (var i = 0; i < $scope.robots.length; ++i) { if ($scope.robots[i].name == name) { return i; } } return -1; }; $scope.getShortenedName = function(name) { var plus = name.indexOf('+'); return name.substr(plus + 1); }; $scope.getPrefix = function(name) { var plus = name.indexOf('+'); return name.substr(0, plus); }; $scope.createRobot = function(name) { if (!name) { return; } createRobotAccount(ApiService, !!$scope.organization, $scope.organization ? $scope.organization.name : '', name, function(created) { $scope.robots.push(created); }); }; $scope.deleteRobot = function(info) { var shortName = $scope.getShortenedName(info.name); ApiService.deleteRobot($scope.organization, null, {'robot_shortname': shortName}).then(function(resp) { var index = $scope.findRobotIndexByName(info.name); if (index >= 0) { $scope.robots.splice(index, 1); } }, ApiService.errorDisplay('Cannot delete robot account')); }; var update = function() { if (!$scope.user && !$scope.organization) { return; } if ($scope.loading) { return; } $scope.loading = true; ApiService.getRobots($scope.organization).then(function(resp) { $scope.robots = resp.robots; $scope.loading = false; if ($routeParams.showRobot) { var index = $scope.findRobotIndexByName($routeParams.showRobot); if (index >= 0) { $scope.showRobot($scope.robots[index]); } } }); }; $scope.$watch('organization', update); $scope.$watch('user', update); } }; return directiveDefinitionObject; }); quayApp.directive('prototypeManager', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/prototype-manager.html', replace: false, transclude: false, restrict: 'C', scope: { 'organization': '=organization' }, controller: function($scope, $element, ApiService) { $scope.loading = false; $scope.activatingForNew = null; $scope.delegateForNew = null; $scope.clearCounter = 0; $scope.newForWholeOrg = true; $scope.roles = [ { 'id': 'read', 'title': 'Read', 'kind': 'success' }, { 'id': 'write', 'title': 'Write', 'kind': 'success' }, { 'id': 'admin', 'title': 'Admin', 'kind': 'primary' } ]; $scope.setRole = function(role, prototype) { var params = { 'orgname': $scope.organization.name, 'prototypeid': prototype.id }; var data = { 'id': prototype.id, 'role': role }; ApiService.updateOrganizationPrototypePermission(data, params).then(function(resp) { prototype.role = role; }, ApiService.errorDisplay('Cannot modify permission')); }; $scope.comparePrototypes = function(p) { return p.activating_user ? p.activating_user.name : ' '; }; $scope.setRoleForNew = function(role) { $scope.newRole = role; }; $scope.setNewForWholeOrg = function(value) { $scope.newForWholeOrg = value; }; $scope.showAddDialog = function() { $scope.activatingForNew = null; $scope.delegateForNew = null; $scope.newRole = 'read'; $scope.clearCounter++; $scope.newForWholeOrg = true; $('#addPermissionDialogModal').modal({}); }; $scope.createPrototype = function() { $scope.loading = true; var params = { 'orgname': $scope.organization.name }; var data = { 'delegate': $scope.delegateForNew, 'role': $scope.newRole }; if (!$scope.newForWholeOrg) { data['activating_user'] = $scope.activatingForNew; } var errorHandler = ApiService.errorDisplay('Cannot create permission', function(resp) { $('#addPermissionDialogModal').modal('hide'); }); ApiService.createOrganizationPrototypePermission(data, params).then(function(resp) { $scope.prototypes.push(resp); $scope.loading = false; $('#addPermissionDialogModal').modal('hide'); }, errorHandler); }; $scope.deletePrototype = function(prototype) { $scope.loading = true; var params = { 'orgname': $scope.organization.name, 'prototypeid': prototype.id }; ApiService.deleteOrganizationPrototypePermission(null, params).then(function(resp) { $scope.prototypes.splice($scope.prototypes.indexOf(prototype), 1); $scope.loading = false; }, ApiService.errorDisplay('Cannot delete permission')); }; var update = function() { if (!$scope.organization) { return; } if ($scope.loading) { return; } var params = {'orgname': $scope.organization.name}; $scope.loading = true; ApiService.getOrganizationPrototypePermissions(null, params).then(function(resp) { $scope.prototypes = resp.prototypes; $scope.loading = false; }); }; $scope.$watch('organization', update); } }; return directiveDefinitionObject; }); quayApp.directive('deleteUi', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/delete-ui.html', replace: false, transclude: true, restrict: 'C', scope: { 'deleteTitle': '=deleteTitle', 'buttonTitle': '=buttonTitle', 'performDelete': '&performDelete' }, controller: function($scope, $element) { $scope.buttonTitleInternal = $scope.buttonTitle || 'Delete'; $element.children().attr('tabindex', 0); $scope.focus = function() { $element[0].firstChild.focus(); }; } }; return directiveDefinitionObject; }); quayApp.directive('popupInputButton', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/popup-input-button.html', replace: false, transclude: true, restrict: 'C', scope: { 'placeholder': '=placeholder', 'pattern': '=pattern', 'submitted': '&submitted' }, controller: function($scope, $element) { $scope.popupShown = function() { setTimeout(function() { var box = $('#input-box'); box[0].value = ''; box.focus(); }, 40); }; $scope.getRegexp = function(pattern) { if (!pattern) { pattern = '.*'; } return new RegExp(pattern); }; $scope.inputSubmit = function() { var box = $('#input-box'); if (box.hasClass('ng-invalid')) { return; } var entered = box[0].value; if (!entered) { return; } if ($scope.submitted) { $scope.submitted({'value': entered}); } }; } }; return directiveDefinitionObject; }); quayApp.directive('resourceView', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/resource-view.html', replace: false, transclude: true, restrict: 'C', scope: { 'resource': '=resource', 'errorMessage': '=errorMessage' }, controller: function($scope, $element) { } }; return directiveDefinitionObject; }); quayApp.directive('quaySpinner', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/spinner.html', replace: false, transclude: true, restrict: 'C', scope: {}, controller: function($scope, $element) { } }; return directiveDefinitionObject; }); quayApp.directive('registryName', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/registry-name.html', replace: false, transclude: true, restrict: 'C', scope: {}, controller: function($scope, $element, Config) { $scope.name = Config.REGISTRY_TITLE; } }; return directiveDefinitionObject; }); quayApp.directive('organizationHeader', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/organization-header.html', replace: false, transclude: true, restrict: 'C', scope: { 'organization': '=organization', 'teamName': '=teamName', 'clickable': '=clickable' }, controller: function($scope, $element) { } }; return directiveDefinitionObject; }); quayApp.directive('markdownInput', function () { var counter = 0; var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/markdown-input.html', replace: false, transclude: false, restrict: 'C', scope: { 'content': '=content', 'canWrite': '=canWrite', 'contentChanged': '=contentChanged', 'fieldTitle': '=fieldTitle' }, controller: function($scope, $element) { var elm = $element[0]; $scope.id = (counter++); $scope.editContent = function() { if (!$scope.canWrite) { return; } if (!$scope.markdownDescriptionEditor) { var converter = Markdown.getSanitizingConverter(); var editor = new Markdown.Editor(converter, '-description-' + $scope.id); editor.run(); $scope.markdownDescriptionEditor = editor; } $('#wmd-input-description-' + $scope.id)[0].value = $scope.content; $(elm).find('.modal').modal({}); }; $scope.saveContent = function() { $scope.content = $('#wmd-input-description-' + $scope.id)[0].value; $(elm).find('.modal').modal('hide'); if ($scope.contentChanged) { $scope.contentChanged($scope.content); } }; } }; return directiveDefinitionObject; }); quayApp.directive('repoSearch', function () { var number = 0; var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/repo-search.html', replace: false, transclude: false, restrict: 'C', scope: { }, controller: function($scope, $element, $location, UserService, Restangular) { var searchToken = 0; $scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) { ++searchToken; }, true); var repoHound = new Bloodhound({ name: 'repositories', remote: { url: '/api/v1/find/repository?query=%QUERY', replace: function (url, uriEncodedQuery) { url = url.replace('%QUERY', uriEncodedQuery); url += '&cb=' + searchToken; return url; }, filter: function(data) { var datums = []; for (var i = 0; i < data.repositories.length; ++i) { var repo = data.repositories[i]; datums.push({ 'value': repo.name, 'tokens': [repo.name, repo.namespace], 'repo': repo }); } return datums; } }, datumTokenizer: function(d) { return Bloodhound.tokenizers.whitespace(d.val); }, queryTokenizer: Bloodhound.tokenizers.whitespace }); repoHound.initialize(); var element = $($element[0].childNodes[0]); element.typeahead({ 'highlight': true }, { source: repoHound.ttAdapter(), templates: { 'suggestion': function (datum) { template = '
'; template += '' template += '' + datum.repo.namespace +'/' + datum.repo.name + '' if (datum.repo.description) { template += '' + getFirstTextLine(datum.repo.description) + '' } template += '
' return template; } } }); element.on('typeahead:selected', function (e, datum) { element.typeahead('val', ''); $scope.$apply(function() { $location.path('/repository/' + datum.repo.namespace + '/' + datum.repo.name); }); }); } }; return directiveDefinitionObject; }); quayApp.directive('headerBar', function () { var number = 0; var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/header-bar.html', replace: false, transclude: false, restrict: 'C', scope: { }, controller: function($scope, $element, $location, UserService, PlanService, ApiService, NotificationService) { $scope.notificationService = NotificationService; // Monitor any user changes and place the current user into the scope. UserService.updateUserIn($scope); $scope.signout = function() { ApiService.logout().then(function() { UserService.load(); $location.path('/'); }); }; $scope.appLinkTarget = function() { if ($("div[ng-view]").length === 0) { return "_self"; } return ""; }; } }; return directiveDefinitionObject; }); quayApp.directive('entitySearch', function () { var number = 0; var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/entity-search.html', replace: false, transclude: false, restrict: 'C', require: '?ngModel', link: function(scope, element, attr, ctrl) { scope.ngModel = ctrl; }, scope: { 'namespace': '=namespace', 'placeholder': '=placeholder', // Default: ['user', 'team', 'robot'] 'allowedEntities': '=allowedEntities', 'currentEntity': '=currentEntity', 'entitySelected': '&entitySelected', // When set to true, the contents of the control will be cleared as soon // as an entity is selected. 'autoClear': '=autoClear', // Set this property to immediately clear the contents of the control. 'clearValue': '=clearValue', }, controller: function($rootScope, $scope, $element, Restangular, UserService, ApiService, Config) { $scope.lazyLoading = true; $scope.teams = null; $scope.robots = null; $scope.isAdmin = false; $scope.isOrganization = false; $scope.includeTeams = true; $scope.includeRobots = true; $scope.includeOrgs = false; $scope.currentEntityInternal = $scope.currentEntity; var isSupported = function(kind, opt_array) { return $.inArray(kind, opt_array || $scope.allowedEntities || ['user', 'team', 'robot']) >= 0; }; $scope.lazyLoad = function() { if (!$scope.namespace || !$scope.lazyLoading) { return; } // Reset the cached teams and robots. $scope.teams = null; $scope.robots = null; // Load the organization's teams (if applicable). if ($scope.isOrganization && isSupported('team')) { // Note: We load the org here again so that we always have the fully up-to-date // teams list. ApiService.getOrganization(null, {'orgname': $scope.namespace}).then(function(resp) { $scope.teams = resp.teams; }); } // Load the user/organization's robots (if applicable). if ($scope.isAdmin && isSupported('robot')) { ApiService.getRobots($scope.isOrganization ? $scope.namespace : null).then(function(resp) { $scope.robots = resp.robots; $scope.lazyLoading = false; }, function() { $scope.lazyLoading = false; }); } else { $scope.lazyLoading = false; } }; $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; } createOrganizationTeam(ApiService, $scope.namespace, teamname, function(created) { $scope.setEntity(created.name, 'team', false); $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; } createRobotAccount(ApiService, $scope.isOrganization, $scope.namespace, robotname, function(created) { $scope.setEntity(created.name, 'user', true); $scope.robots.push(created); }); }); }; $scope.setEntity = function(name, kind, is_robot) { var entity = { 'name': name, 'kind': kind, 'is_robot': is_robot }; if ($scope.isOrganization) { entity['is_org_member'] = true; } $scope.setEntityInternal(entity, false); }; $scope.clearEntityInternal = function() { $scope.currentEntityInternal = null; $scope.currentEntity = null; $scope.entitySelected({'entity': null}); if ($scope.ngModel) { $scope.ngModel.$setValidity('entity', false); } }; $scope.setEntityInternal = function(entity, updateTypeahead) { if (updateTypeahead) { $(input).typeahead('val', $scope.autoClear ? '' : entity.name); } else { $(input).val($scope.autoClear ? '' : entity.name); } if (!$scope.autoClear) { $scope.currentEntityInternal = entity; $scope.currentEntity = entity; } $scope.entitySelected({'entity': entity}); if ($scope.ngModel) { $scope.ngModel.$setValidity('entity', !!entity); } }; // Setup the typeahead. var input = $element[0].firstChild.firstChild; (function() { // Create the bloodhound search query system. $rootScope.__entity_search_counter = (($rootScope.__entity_search_counter || 0) + 1); var entitySearchB = new Bloodhound({ name: 'entities' + $rootScope.__entity_search_counter, remote: { url: '/api/v1/entities/%QUERY', replace: function (url, uriEncodedQuery) { var namespace = $scope.namespace || ''; url = url.replace('%QUERY', uriEncodedQuery); url += '?namespace=' + encodeURIComponent(namespace); if ($scope.isOrganization && isSupported('team')) { url += '&includeTeams=true' } if (isSupported('org')) { url += '&includeOrgs=true' } return url; }, filter: function(data) { var datums = []; for (var i = 0; i < data.results.length; ++i) { var entity = data.results[i]; var found = 'user'; if (entity.kind == 'user') { found = entity.is_robot ? 'robot' : 'user'; } else if (entity.kind == 'team') { found = 'team'; } else if (entity.kind == 'org') { found = 'org'; } if (!isSupported(found)) { continue; } datums.push({ 'value': entity.name, 'tokens': [entity.name], 'entity': entity }); } return datums; } }, datumTokenizer: function(d) { return Bloodhound.tokenizers.whitespace(d.val); }, queryTokenizer: Bloodhound.tokenizers.whitespace }); entitySearchB.initialize(); // Setup the typeahead. $(input).typeahead({ 'highlight': true }, { source: entitySearchB.ttAdapter(), templates: { 'empty': function(info) { // Only display the empty dialog if the server load has finished. if (info.resultKind == 'remote') { var val = $(input).val(); if (!val) { return null; } if (val.indexOf('@') > 0) { return '
A ' + Config.REGISTRY_TITLE_SHORT + ' username (not an e-mail address) must be specified
'; } var classes = []; if (isSupported('user')) { classes.push('users'); } if (isSupported('org')) { classes.push('organizations'); } if ($scope.isAdmin && isSupported('robot')) { classes.push('robot accounts'); } if ($scope.isOrganization && isSupported('team')) { classes.push('teams'); } if (classes.length > 1) { classes[classes.length - 1] = 'or ' + classes[classes.length - 1]; } var class_string = ''; for (var i = 0; i < classes.length; ++i) { if (i > 0) { if (i == classes.length - 1) { class_string += ' or '; } else { class_string += ', '; } } class_string += classes[i]; } return '
No matching ' + Config.REGISTRY_TITLE_SHORT + ' ' + class_string + ' found
'; } return null; }, 'suggestion': function (datum) { template = '
'; if (datum.entity.kind == 'user' && !datum.entity.is_robot) { template += ''; } else if (datum.entity.kind == 'user' && datum.entity.is_robot) { template += ''; } else if (datum.entity.kind == 'team') { template += ''; } else if (datum.entity.kind == 'org') { template += ''; } template += '' + datum.value + ''; if (datum.entity.is_org_member === false && datum.entity.kind == 'user') { template += ''; } template += '
'; return template; }} }); $(input).on('input', function(e) { $scope.$apply(function() { $scope.clearEntityInternal(); }); }); $(input).on('typeahead:selected', function(e, datum) { $scope.$apply(function() { $scope.setEntityInternal(datum.entity, true); }); }); })(); $scope.$watch('clearValue', function() { if (!input) { return; } $(input).typeahead('val', ''); $scope.clearEntityInternal(); }); $scope.$watch('placeholder', function(title) { input.setAttribute('placeholder', title); }); $scope.$watch('allowedEntities', function(allowed) { if (!allowed) { return; } $scope.includeTeams = isSupported('team', allowed); $scope.includeRobots = isSupported('robot', allowed); }); $scope.$watch('namespace', function(namespace) { if (!namespace) { return; } $scope.isAdmin = UserService.isNamespaceAdmin(namespace); $scope.isOrganization = !!UserService.getOrganization(namespace); }); $scope.$watch('currentEntity', function(entity) { if ($scope.currentEntityInternal != entity) { if (entity) { $scope.setEntityInternal(entity, false); } else { $scope.clearEntityInternal(); } } }); } }; return directiveDefinitionObject; }); quayApp.directive('roleGroup', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/role-group.html', replace: false, transclude: false, restrict: 'C', scope: { 'roles': '=roles', 'currentRole': '=currentRole', 'roleChanged': '&roleChanged' }, controller: function($scope, $element) { $scope.setRole = function(role) { if ($scope.currentRole == role) { return; } if ($scope.roleChanged) { $scope.roleChanged({'role': role}); } else { $scope.currentRole = role; } }; } }; return directiveDefinitionObject; }); quayApp.directive('billingOptions', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/billing-options.html', replace: false, transclude: false, restrict: 'C', scope: { 'user': '=user', 'organization': '=organization' }, controller: function($scope, $element, PlanService, ApiService) { $scope.invoice_email = false; $scope.currentCard = null; // Listen to plan changes. PlanService.registerListener(this, function(plan) { if (plan && plan.price > 0) { update(); } }); $scope.$on('$destroy', function() { PlanService.unregisterListener(this); }); $scope.isExpiringSoon = function(cardInfo) { var current = new Date(); var expires = new Date(cardInfo.exp_year, cardInfo.exp_month, 1); var difference = expires - current; return difference < (60 * 60 * 24 * 60 * 1000 /* 60 days */); }; $scope.changeCard = function() { var previousCard = $scope.currentCard; $scope.changingCard = true; var callbacks = { 'opened': function() { $scope.changingCard = true; }, 'closed': function() { $scope.changingCard = false; }, 'started': function() { $scope.currentCard = null; }, 'success': function(resp) { $scope.currentCard = resp.card; $scope.changingCard = false; }, 'failure': function(resp) { $scope.changingCard = false; $scope.currentCard = previousCard; if (!PlanService.isCardError(resp)) { $('#cannotchangecardModal').modal({}); } } }; PlanService.changeCreditCard($scope, $scope.organization ? $scope.organization.name : null, callbacks); }; $scope.getCreditImage = function(creditInfo) { if (!creditInfo || !creditInfo.type) { return 'credit.png'; } var kind = creditInfo.type.toLowerCase() || 'credit'; var supported = { 'american express': 'amex', 'credit': 'credit', 'diners club': 'diners', 'discover': 'discover', 'jcb': 'jcb', 'mastercard': 'mastercard', 'visa': 'visa' }; kind = supported[kind] || 'credit'; return kind + '.png'; }; var update = function() { if (!$scope.user && !$scope.organization) { return; } $scope.obj = $scope.user ? $scope.user : $scope.organization; $scope.invoice_email = $scope.obj.invoice_email; // Load the credit card information. PlanService.getCardInfo($scope.organization ? $scope.organization.name : null, function(card) { $scope.currentCard = card; }); }; var save = function() { $scope.working = true; var errorHandler = ApiService.errorDisplay('Could not change user details'); ApiService.changeDetails($scope.organization, $scope.obj).then(function(resp) { $scope.working = false; }, errorHandler); }; var checkSave = function() { if (!$scope.obj) { return; } if ($scope.obj.invoice_email != $scope.invoice_email) { $scope.obj.invoice_email = $scope.invoice_email; save(); } }; $scope.$watch('invoice_email', checkSave); $scope.$watch('organization', update); $scope.$watch('user', update); } }; return directiveDefinitionObject; }); quayApp.directive('planManager', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/plan-manager.html', replace: false, transclude: false, restrict: 'C', scope: { 'user': '=user', 'organization': '=organization', 'readyForPlan': '&readyForPlan', 'planChanged': '&planChanged' }, controller: function($scope, $element, PlanService, ApiService) { $scope.isExistingCustomer = false; $scope.parseDate = function(timestamp) { return new Date(timestamp * 1000); }; $scope.isPlanVisible = function(plan, subscribedPlan) { if (plan['deprecated']) { return plan == subscribedPlan; } if ($scope.organization && !PlanService.isOrgCompatible(plan)) { return false; } return true; }; $scope.changeSubscription = function(planId, opt_async) { if ($scope.planChanging) { return; } var callbacks = { 'opening': function() { $scope.planChanging = true; }, 'started': function() { $scope.planChanging = true; }, 'opened': function() { $scope.planChanging = true; }, 'closed': function() { $scope.planChanging = false; }, 'success': subscribedToPlan, 'failure': function(resp) { $scope.planChanging = false; } }; PlanService.changePlan($scope, $scope.organization, planId, callbacks, opt_async); }; $scope.cancelSubscription = function() { $scope.changeSubscription(PlanService.getFreePlan()); }; var subscribedToPlan = function(sub) { $scope.subscription = sub; $scope.isExistingCustomer = !!sub['isExistingCustomer']; PlanService.getPlanIncludingDeprecated(sub.plan, function(subscribedPlan) { $scope.subscribedPlan = subscribedPlan; $scope.planUsagePercent = sub.usedPrivateRepos * 100 / $scope.subscribedPlan.privateRepos; if ($scope.planChanged) { $scope.planChanged({ 'plan': subscribedPlan }); } if (sub.usedPrivateRepos > $scope.subscribedPlan.privateRepos) { $scope.limit = 'over'; } else if (sub.usedPrivateRepos == $scope.subscribedPlan.privateRepos) { $scope.limit = 'at'; } else if (sub.usedPrivateRepos >= $scope.subscribedPlan.privateRepos * 0.7) { $scope.limit = 'near'; } else { $scope.limit = 'none'; } if (!$scope.chart) { $scope.chart = new UsageChart(); $scope.chart.draw('repository-usage-chart'); } $scope.chart.update(sub.usedPrivateRepos || 0, $scope.subscribedPlan.privateRepos || 0); $scope.planChanging = false; $scope.planLoading = false; }); }; var update = function() { $scope.planLoading = true; if (!$scope.plans) { return; } PlanService.getSubscription($scope.organization, subscribedToPlan, function() { $scope.isExistingCustomer = false; subscribedToPlan({ 'plan': PlanService.getFreePlan() }); }); }; var loadPlans = function() { if ($scope.plans || $scope.loadingPlans) { return; } if (!$scope.user && !$scope.organization) { return; } $scope.loadingPlans = true; PlanService.verifyLoaded(function(plans) { $scope.plans = plans; update(); if ($scope.readyForPlan) { var planRequested = $scope.readyForPlan(); if (planRequested && planRequested != PlanService.getFreePlan()) { $scope.changeSubscription(planRequested, /* async */true); } } }); }; // Start the initial download. $scope.planLoading = true; loadPlans(); $scope.$watch('organization', loadPlans); $scope.$watch('user', loadPlans); } }; return directiveDefinitionObject; }); quayApp.directive('namespaceSelector', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/namespace-selector.html', replace: false, transclude: false, restrict: 'C', scope: { 'user': '=user', 'namespace': '=namespace', 'requireCreate': '=requireCreate' }, controller: function($scope, $element, $routeParams, $location, CookieService) { $scope.namespaces = {}; $scope.initialize = function(user) { var preferredNamespace = user.username; var namespaces = {}; namespaces[user.username] = user; if (user.organizations) { for (var i = 0; i < user.organizations.length; ++i) { namespaces[user.organizations[i].name] = user.organizations[i]; if (user.organizations[i].preferred_namespace) { preferredNamespace = user.organizations[i].name; } } } var initialNamespace = $routeParams['namespace'] || CookieService.get('quay.namespace') || preferredNamespace || $scope.user.username; $scope.namespaces = namespaces; $scope.setNamespace($scope.namespaces[initialNamespace]); }; $scope.setNamespace = function(namespaceObj) { if (!namespaceObj) { namespaceObj = $scope.namespaces[$scope.user.username]; } if ($scope.requireCreate && !namespaceObj.can_create_repo) { namespaceObj = $scope.namespaces[$scope.user.username]; } var newNamespace = namespaceObj.name || namespaceObj.username; $scope.namespaceObj = namespaceObj; $scope.namespace = newNamespace; if (newNamespace) { CookieService.putPermanent('quay.namespace', newNamespace); if ($routeParams['namespace'] && $routeParams['namespace'] != newNamespace) { $location.search({'namespace': newNamespace}); } } }; $scope.$watch('namespace', function(namespace) { if ($scope.namespaceObj && namespace && namespace != $scope.namespaceObj.username) { $scope.setNamespace($scope.namespaces[namespace]); } }); $scope.$watch('user', function(user) { $scope.user = user; $scope.initialize(user); }); } }; return directiveDefinitionObject; }); quayApp.directive('buildLogPhase', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/build-log-phase.html', replace: false, transclude: false, restrict: 'C', scope: { 'phase': '=phase' }, controller: function($scope, $element) { } }; return directiveDefinitionObject; }); quayApp.directive('buildLogError', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/build-log-error.html', replace: false, transclude: false, restrict: 'C', scope: { 'error': '=error', 'entries': '=entries' }, controller: function($scope, $element, Config) { $scope.getLocalPullInfo = function() { if ($scope.entries.__localpull !== undefined) { return $scope.entries.__localpull; } var localInfo = { 'isLocal': false }; // Find the 'pulling' phase entry, and then extra any metadata found under // it. for (var i = 0; i < $scope.entries.length; ++i) { var entry = $scope.entries[i]; if (entry.type == 'phase' && entry.message == 'pulling') { for (var j = 0; j < entry.logs.length(); ++j) { var log = entry.logs.get(j); if (log.data && log.data.phasestep == 'login') { localInfo['login'] = log.data; } if (log.data && log.data.phasestep == 'pull') { var repo_url = log.data['repo_url']; var repo_and_tag = repo_url.substring(Config.SERVER_HOSTNAME.length + 1); var tagIndex = repo_and_tag.lastIndexOf(':'); var repo = repo_and_tag.substring(0, tagIndex); localInfo['repo_url'] = repo_url; localInfo['repo'] = repo; localInfo['isLocal'] = repo_url.indexOf(Config.SERVER_HOSTNAME + '/') == 0; } } break; } } return $scope.entries.__localpull = localInfo; }; } }; return directiveDefinitionObject; }); quayApp.directive('triggerDescription', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/trigger-description.html', replace: false, transclude: false, restrict: 'C', scope: { 'trigger': '=trigger', 'short': '=short' }, controller: function($scope, $element) { } }; return directiveDefinitionObject; }); quayApp.directive('dropdownSelect', function ($compile) { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/dropdown-select.html', replace: true, transclude: true, restrict: 'C', scope: { 'selectedItem': '=selectedItem', 'placeholder': '=placeholder', 'lookaheadItems': '=lookaheadItems', 'allowCustomInput': '@allowCustomInput', 'handleItemSelected': '&handleItemSelected', 'handleInput': '&handleInput', 'clearValue': '=clearValue' }, controller: function($scope, $element, $rootScope) { if (!$rootScope.__dropdownSelectCounter) { $rootScope.__dropdownSelectCounter = 1; } $scope.placeholder = $scope.placeholder || ''; $scope.internalItem = null; // Setup lookahead. var input = $($element).find('.lookahead-input'); $scope.$watch('clearValue', function(cv) { if (cv) { $scope.selectedItem = null; $(input).val(''); } }); $scope.$watch('selectedItem', function(item) { if ($scope.selectedItem == $scope.internalItem) { // The item has already been set due to an internal action. return; } if ($scope.selectedItem != null) { $(input).val(item.toString()); } else { $(input).val(''); } }); $scope.$watch('lookaheadItems', function(items) { $(input).off(); if (!items) { return; } var formattedItems = []; for (var i = 0; i < items.length; ++i) { var formattedItem = items[i]; if (typeof formattedItem == 'string') { formattedItem = { 'value': formattedItem }; } formattedItems.push(formattedItem); } var dropdownHound = new Bloodhound({ name: 'dropdown-items-' + $rootScope.__dropdownSelectCounter, local: formattedItems, datumTokenizer: function(d) { return Bloodhound.tokenizers.whitespace(d.val || d.value || ''); }, queryTokenizer: Bloodhound.tokenizers.whitespace }); dropdownHound.initialize(); $(input).typeahead({}, { source: dropdownHound.ttAdapter(), templates: { 'suggestion': function (datum) { template = datum['template'] ? datum['template'](datum) : datum['value']; return template; } } }); $(input).on('input', function(e) { $scope.$apply(function() { $scope.internalItem = null; $scope.selectedItem = null; if ($scope.handleInput) { $scope.handleInput({'input': $(input).val()}); } }); }); $(input).on('typeahead:selected', function(e, datum) { $scope.$apply(function() { $scope.internalItem = datum['item'] || datum['value']; $scope.selectedItem = datum['item'] || datum['value']; if ($scope.handleItemSelected) { $scope.handleItemSelected({'datum': datum}); } }); }); $rootScope.__dropdownSelectCounter++; }); }, link: function(scope, element, attrs) { var transcludedBlock = element.find('div.transcluded'); var transcludedElements = transcludedBlock.children(); var iconContainer = element.find('div.dropdown-select-icon-transclude'); var menuContainer = element.find('div.dropdown-select-menu-transclude'); angular.forEach(transcludedElements, function(elem) { if (angular.element(elem).hasClass('dropdown-select-icon')) { iconContainer.append(elem); } else if (angular.element(elem).hasClass('dropdown-select-menu')) { menuContainer.replaceWith(elem); } }); transcludedBlock.remove(); } }; return directiveDefinitionObject; }); quayApp.directive('dropdownSelectIcon', function () { var directiveDefinitionObject = { priority: 1, require: '^dropdownSelect', templateUrl: '/static/directives/dropdown-select-icon.html', replace: false, transclude: false, restrict: 'C', scope: { }, controller: function($scope, $element) { } }; return directiveDefinitionObject; }); quayApp.directive('dropdownSelectMenu', function () { var directiveDefinitionObject = { priority: 1, require: '^dropdownSelect', templateUrl: '/static/directives/dropdown-select-menu.html', replace: true, transclude: true, restrict: 'C', scope: { }, controller: function($scope, $element) { } }; return directiveDefinitionObject; }); quayApp.directive('setupTriggerDialog', function () { var directiveDefinitionObject = { templateUrl: '/static/directives/setup-trigger-dialog.html', replace: false, transclude: false, restrict: 'C', scope: { 'repository': '=repository', 'trigger': '=trigger', 'counter': '=counter', 'canceled': '&canceled', 'activated': '&activated' }, controller: function($scope, $element, ApiService, UserService) { var modalSetup = false; $scope.show = function() { if (!$scope.trigger || !$scope.repository) { return; } $scope.activating = false; $scope.pullEntity = null; $scope.publicPull = true; $scope.showPullRequirements = false; $('#setupTriggerModal').modal({}); if (!modalSetup) { $('#setupTriggerModal').on('hidden.bs.modal', function () { if (!$scope.trigger || $scope.trigger['is_active']) { return; } $scope.$apply(function() { $scope.cancelSetupTrigger(); }); }); modalSetup = true; } }; $scope.isNamespaceAdmin = function(namespace) { return UserService.isNamespaceAdmin(namespace); }; $scope.cancelSetupTrigger = function() { $scope.canceled({'trigger': $scope.trigger}); }; $scope.hide = function() { $scope.activating = false; $('#setupTriggerModal').modal('hide'); }; $scope.setPublicPull = function(value) { $scope.publicPull = value; }; $scope.checkAnalyze = function(isValid) { if (!isValid) { $scope.publicPull = true; $scope.pullEntity = null; $scope.showPullRequirements = false; $scope.checkingPullRequirements = false; return; } $scope.checkingPullRequirements = true; $scope.showPullRequirements = true; $scope.pullRequirements = null; var params = { 'repository': $scope.repository.namespace + '/' + $scope.repository.name, 'trigger_uuid': $scope.trigger.id }; var data = { 'config': $scope.trigger.config }; ApiService.analyzeBuildTrigger(data, params).then(function(resp) { $scope.pullRequirements = resp; if (resp['status'] == 'publicbase') { $scope.publicPull = true; $scope.pullEntity = null; } else if (resp['namespace']) { $scope.publicPull = false; if (resp['robots'] && resp['robots'].length > 0) { $scope.pullEntity = resp['robots'][0]; } else { $scope.pullEntity = null; } } $scope.checkingPullRequirements = false; }, function(resp) { $scope.pullRequirements = resp; $scope.checkingPullRequirements = false; }); }; $scope.activate = function() { var params = { 'repository': $scope.repository.namespace + '/' + $scope.repository.name, 'trigger_uuid': $scope.trigger.id }; var data = { 'config': $scope.trigger['config'] }; if ($scope.pullEntity) { data['pull_robot'] = $scope.pullEntity['name']; } $scope.activating = true; var errorHandler = ApiService.errorDisplay('Cannot activate build trigger', function(resp) { $scope.hide(); $scope.canceled({'trigger': $scope.trigger}); }); ApiService.activateBuildTrigger(data, params).then(function(resp) { $scope.hide(); $scope.trigger['is_active'] = true; $scope.trigger['pull_robot'] = resp['pull_robot']; $scope.activated({'trigger': $scope.trigger}); }, errorHandler); }; var check = function() { if ($scope.counter && $scope.trigger && $scope.repository) { $scope.show(); } }; $scope.$watch('trigger', check); $scope.$watch('counter', check); $scope.$watch('repository', check); } }; return directiveDefinitionObject; }); quayApp.directive('triggerSetupGithub', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/trigger-setup-github.html', replace: false, transclude: false, restrict: 'C', scope: { 'repository': '=repository', 'trigger': '=trigger', 'analyze': '&analyze' }, controller: function($scope, $element, ApiService) { $scope.analyzeCounter = 0; $scope.setupReady = false; $scope.loading = true; $scope.handleLocationInput = function(location) { $scope.trigger['config']['subdir'] = location || ''; $scope.isInvalidLocation = $scope.locations.indexOf(location) < 0; $scope.analyze({'isValid': !$scope.isInvalidLocation}); }; $scope.handleLocationSelected = function(datum) { $scope.setLocation(datum['value']); }; $scope.setLocation = function(location) { $scope.currentLocation = location; $scope.trigger['config']['subdir'] = location || ''; $scope.isInvalidLocation = false; $scope.analyze({'isValid': true}); }; $scope.selectRepo = function(repo, org) { $scope.currentRepo = { 'repo': repo, 'avatar_url': org['info']['avatar_url'], 'toString': function() { return this.repo; } }; }; $scope.selectRepoInternal = function(currentRepo) { if (!currentRepo) { $scope.trigger.$ready = false; return; } var params = { 'repository': $scope.repository.namespace + '/' + $scope.repository.name, 'trigger_uuid': $scope.trigger['id'] }; var repo = currentRepo['repo']; $scope.trigger['config'] = { 'build_source': repo, 'subdir': '' }; // Lookup the possible Dockerfile locations. $scope.locations = null; if (repo) { ApiService.listBuildTriggerSubdirs($scope.trigger['config'], params).then(function(resp) { if (resp['status'] == 'error') { $scope.locationError = resp['message'] || 'Could not load Dockerfile locations'; $scope.locations = null; $scope.trigger.$ready = false; $scope.isInvalidLocation = false; $scope.analyze({'isValid': false}); return; } $scope.locationError = null; $scope.locations = resp['subdir'] || []; $scope.trigger.$ready = true; if ($scope.locations.length > 0) { $scope.setLocation($scope.locations[0]); } else { $scope.currentLocation = null; $scope.isInvalidLocation = resp['subdir'].indexOf('') < 0; $scope.analyze({'isValid': !$scope.isInvalidLocation}); } }, function(resp) { $scope.locationError = resp['message'] || 'Could not load Dockerfile locations'; $scope.locations = null; $scope.trigger.$ready = false; $scope.isInvalidLocation = false; $scope.analyze({'isValid': false}); }); } }; var setupTypeahead = function() { var repos = []; for (var i = 0; i < $scope.orgs.length; ++i) { var org = $scope.orgs[i]; var orepos = org['repos']; for (var j = 0; j < orepos.length; ++j) { var repoValue = { 'repo': orepos[j], 'avatar_url': org['info']['avatar_url'], 'toString': function() { return this.repo; } }; var datum = { 'name': orepos[j], 'org': org, 'value': orepos[j], 'title': orepos[j], 'item': repoValue }; repos.push(datum); } } $scope.repoLookahead = repos; }; var loadSources = function() { var params = { 'repository': $scope.repository.namespace + '/' + $scope.repository.name, 'trigger_uuid': $scope.trigger.id }; ApiService.listTriggerBuildSources(null, params).then(function(resp) { $scope.orgs = resp['sources']; setupTypeahead(); $scope.loading = false; }); }; var check = function() { if ($scope.repository && $scope.trigger) { loadSources(); } }; $scope.$watch('repository', check); $scope.$watch('trigger', check); $scope.$watch('currentRepo', function(repo) { $scope.selectRepoInternal(repo); }); } }; return directiveDefinitionObject; }); quayApp.directive('buildLogCommand', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/build-log-command.html', replace: false, transclude: false, restrict: 'C', scope: { 'command': '=command' }, controller: function($scope, $element) { $scope.getWithoutStep = function(fullTitle) { var colon = fullTitle.indexOf(':'); if (colon <= 0) { return ''; } return $.trim(fullTitle.substring(colon + 1)); }; } }; return directiveDefinitionObject; }); quayApp.directive('dockerfileCommand', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/dockerfile-command.html', replace: false, transclude: false, restrict: 'C', scope: { 'command': '=command' }, controller: function($scope, $element, UtilService, Config) { var registryHandlers = { 'quay.io': function(pieces) { var rnamespace = pieces[pieces.length - 2]; var rname = pieces[pieces.length - 1].split(':')[0]; return '/repository/' + rnamespace + '/' + rname + '/'; }, '': function(pieces) { var rnamespace = pieces.length == 1 ? '_' : 'u/' + pieces[0]; var rname = pieces[pieces.length - 1].split(':')[0]; return 'https://registry.hub.docker.com/' + rnamespace + '/' + rname + '/'; } }; registryHandlers[Config.getDomain()] = registryHandlers['quay.io']; var kindHandlers = { 'FROM': function(title) { var pieces = title.split('/'); var registry = pieces.length < 3 ? '' : pieces[0]; if (!registryHandlers[registry]) { return title; } return ' ' + title + ''; } }; $scope.getCommandKind = function(title) { var space = title.indexOf(' '); return title.substring(0, space); }; $scope.getCommandTitleHtml = function(title) { var space = title.indexOf(' '); if (space <= 0) { return UtilService.textToSafeHtml(title); } var kind = $scope.getCommandKind(title); var sanitized = UtilService.textToSafeHtml(title.substring(space + 1)); var handler = kindHandlers[kind || '']; if (handler) { return handler(sanitized); } else { return sanitized; } }; } }; return directiveDefinitionObject; }); quayApp.directive('dockerfileView', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/dockerfile-view.html', replace: false, transclude: false, restrict: 'C', scope: { 'contents': '=contents' }, controller: function($scope, $element, UtilService) { $scope.$watch('contents', function(contents) { $scope.lines = []; var lines = contents ? contents.split('\n') : []; for (var i = 0; i < lines.length; ++i) { var line = $.trim(lines[i]); var kind = 'text'; if (line && line[0] == '#') { kind = 'comment'; } else if (line.match(/^([A-Z]+\s)/)) { kind = 'command'; } var lineInfo = { 'text': line, 'kind': kind }; $scope.lines.push(lineInfo); } }); } }; return directiveDefinitionObject; }); quayApp.directive('buildStatus', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/build-status.html', replace: false, transclude: false, restrict: 'C', scope: { 'build': '=build' }, controller: function($scope, $element) { } }; return directiveDefinitionObject; }); quayApp.directive('buildMessage', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/build-message.html', replace: false, transclude: false, restrict: 'C', scope: { 'phase': '=phase' }, controller: function($scope, $element) { $scope.getBuildMessage = function (phase) { switch (phase) { case 'cannot_load': return 'Cannot load build status - Please report this error'; case 'starting': case 'initializing': return 'Starting Dockerfile build'; case 'waiting': return 'Waiting for available build worker'; case 'unpacking': return 'Unpacking build package'; case 'pulling': return 'Pulling base image'; case 'building': return 'Building image from Dockerfile'; case 'pushing': return 'Pushing image built from Dockerfile'; case 'complete': return 'Dockerfile build completed and pushed'; case 'error': return 'Dockerfile build failed'; } }; } }; return directiveDefinitionObject; }); quayApp.directive('buildProgress', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/build-progress.html', replace: false, transclude: false, restrict: 'C', scope: { 'build': '=build' }, controller: function($scope, $element) { $scope.getPercentage = function(buildInfo) { switch (buildInfo.phase) { case 'pulling': return buildInfo.status.pull_completion * 100; break; case 'building': return (buildInfo.status.current_command / buildInfo.status.total_commands) * 100; break; case 'pushing': return buildInfo.status.push_completion * 100; break; case 'complete': return 100; break; case 'initializing': case 'starting': case 'waiting': case 'cannot_load': case 'unpacking': return 0; break; } return -1; }; } }; return directiveDefinitionObject; }); quayApp.directive('externalNotificationView', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/external-notification-view.html', replace: false, transclude: false, restrict: 'C', scope: { 'repository': '=repository', 'notification': '=notification', 'notificationDeleted': '¬ificationDeleted' }, controller: function($scope, $element, ExternalNotificationData, ApiService) { $scope.deleteNotification = function() { var params = { 'repository': $scope.repository.namespace + '/' + $scope.repository.name, 'uuid': $scope.notification.uuid }; ApiService.deleteRepoNotification(null, params).then(function() { $scope.notificationDeleted({'notification': $scope.notification}); }); }; $scope.testNotification = function() { var params = { 'repository': $scope.repository.namespace + '/' + $scope.repository.name, 'uuid': $scope.notification.uuid }; ApiService.testRepoNotification(null, params).then(function() { bootbox.dialog({ "title": "Test Notification Queued", "message": "A test version of this notification has been queued and should appear shortly", "buttons": { "close": { "label": "Close", "className": "btn-primary" } } }); }); }; $scope.$watch('notification', function(notification) { if (notification) { $scope.eventInfo = ExternalNotificationData.getEventInfo(notification.event); $scope.methodInfo = ExternalNotificationData.getMethodInfo(notification.method); $scope.config = notification.config; } }); } }; return directiveDefinitionObject; }); quayApp.directive('createExternalNotificationDialog', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/create-external-notification-dialog.html', replace: false, transclude: false, restrict: 'C', scope: { 'repository': '=repository', 'counter': '=counter', 'notificationCreated': '¬ificationCreated' }, controller: function($scope, $element, ExternalNotificationData, ApiService, $timeout, StringBuilderService) { $scope.currentEvent = null; $scope.currentMethod = null; $scope.status = ''; $scope.currentConfig = {}; $scope.clearCounter = 0; $scope.unauthorizedEmail = false; $scope.events = ExternalNotificationData.getSupportedEvents(); $scope.methods = ExternalNotificationData.getSupportedMethods(); $scope.setEvent = function(event) { $scope.currentEvent = event; }; $scope.setMethod = function(method) { $scope.currentConfig = {}; $scope.currentMethod = method; $scope.unauthorizedEmail = false; }; $scope.createNotification = function() { if (!$scope.currentConfig.email) { $scope.performCreateNotification(); return; } $scope.status = 'checking-email'; $scope.checkEmailAuthorization(); }; $scope.checkEmailAuthorization = function() { var params = { 'repository': $scope.repository.namespace + '/' + $scope.repository.name, 'email': $scope.currentConfig.email }; ApiService.checkRepoEmailAuthorized(null, params).then(function(resp) { $scope.handleEmailCheck(resp.confirmed); }, function(resp) { $scope.handleEmailCheck(false); }); }; $scope.performCreateNotification = function() { $scope.status = 'creating'; var params = { 'repository': $scope.repository.namespace + '/' + $scope.repository.name }; var data = { 'event': $scope.currentEvent.id, 'method': $scope.currentMethod.id, 'config': $scope.currentConfig }; ApiService.createRepoNotification(data, params).then(function(resp) { $scope.status = ''; $scope.notificationCreated({'notification': resp}); $('#createNotificationModal').modal('hide'); }); }; $scope.handleEmailCheck = function(isAuthorized) { if (isAuthorized) { $scope.performCreateNotification(); return; } if ($scope.status == 'authorizing-email-sent') { $scope.watchEmail(); } else { $scope.status = 'unauthorized-email'; } $scope.unauthorizedEmail = true; }; $scope.sendAuthEmail = function() { $scope.status = 'authorizing-email'; var params = { 'repository': $scope.repository.namespace + '/' + $scope.repository.name, 'email': $scope.currentConfig.email }; ApiService.sendAuthorizeRepoEmail(null, params).then(function(resp) { $scope.status = 'authorizing-email-sent'; $scope.watchEmail(); }); }; $scope.watchEmail = function() { // TODO: change this to SSE? $timeout(function() { $scope.checkEmailAuthorization(); }, 1000); }; $scope.getHelpUrl = function(field, config) { var helpUrl = field['help_url']; if (!helpUrl) { return null; } return StringBuilderService.buildUrl(helpUrl, config); }; $scope.$watch('counter', function(counter) { if (counter) { $scope.clearCounter++; $scope.status = ''; $scope.currentEvent = null; $scope.currentMethod = null; $scope.unauthorizedEmail = false; $('#createNotificationModal').modal({}); } }); } }; return directiveDefinitionObject; }); quayApp.directive('twitterView', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/twitter-view.html', replace: false, transclude: true, restrict: 'C', scope: { 'avatarUrl': '@avatarUrl', 'authorName': '@authorName', 'authorUser': '@authorUser', 'messageUrl': '@messageUrl', 'messageDate': '@messageDate' }, controller: function($scope, $element) { } }; return directiveDefinitionObject; }); quayApp.directive('notificationsBubble', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/notifications-bubble.html', replace: false, transclude: false, restrict: 'C', scope: { }, controller: function($scope, UserService, NotificationService) { $scope.notificationService = NotificationService; } }; return directiveDefinitionObject; }); quayApp.directive('notificationView', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/notification-view.html', replace: false, transclude: false, restrict: 'C', scope: { 'notification': '=notification', 'parent': '=parent' }, controller: function($scope, $element, $window, $location, UserService, NotificationService, ApiService) { var stringStartsWith = function (str, prefix) { return str.slice(0, prefix.length) == prefix; }; $scope.getMessage = function(notification) { return NotificationService.getMessage(notification); }; $scope.getGravatar = function(orgname) { var organization = UserService.getOrganization(orgname); return organization['gravatar'] || ''; }; $scope.parseDate = function(dateString) { return Date.parse(dateString); }; $scope.showNotification = function() { var url = NotificationService.getPage($scope.notification); if (url) { if (stringStartsWith(url, 'http://') || stringStartsWith(url, 'https://')) { $window.location.href = url; } else { var parts = url.split('?') $location.path(parts[0]); if (parts.length > 1) { $location.search(parts[1]); } $scope.parent.$hide(); } } }; $scope.dismissNotification = function(notification) { NotificationService.dismissNotification(notification); }; $scope.canDismiss = function(notification) { return NotificationService.canDismiss(notification); }; $scope.getClass = function(notification) { return NotificationService.getClass(notification); }; } }; return directiveDefinitionObject; }); quayApp.directive('dockerfileBuildDialog', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/dockerfile-build-dialog.html', replace: false, transclude: false, restrict: 'C', scope: { 'repository': '=repository', 'showNow': '=showNow', 'buildStarted': '&buildStarted' }, controller: function($scope, $element) { $scope.building = false; $scope.uploading = false; $scope.startCounter = 0; $scope.handleBuildStarted = function(build) { $('#dockerfilebuildModal').modal('hide'); if ($scope.buildStarted) { $scope.buildStarted({'build': build}); } }; $scope.handleBuildFailed = function(message) { $scope.errorMessage = message; }; $scope.startBuild = function() { $scope.errorMessage = null; $scope.startCounter++; }; $scope.$watch('showNow', function(sn) { if (sn && $scope.repository) { $('#dockerfilebuildModal').modal({}); } }); } }; return directiveDefinitionObject; }); quayApp.directive('dockerfileBuildForm', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/dockerfile-build-form.html', replace: false, transclude: false, restrict: 'C', scope: { 'repository': '=repository', 'startNow': '=startNow', 'hasDockerfile': '=hasDockerfile', 'uploadFailed': '&uploadFailed', 'uploadStarted': '&uploadStarted', 'buildStarted': '&buildStarted', 'buildFailed': '&buildFailed', 'missingFile': '&missingFile', 'uploading': '=uploading', 'building': '=building' }, controller: function($scope, $element, ApiService) { $scope.internal = {'hasDockerfile': false}; var handleBuildFailed = function(message) { message = message || 'Dockerfile build failed to start'; var result = false; if ($scope.buildFailed) { result = $scope.buildFailed({'message': message}); } if (!result) { bootbox.dialog({ "message": message, "title": "Cannot start Dockerfile build", "buttons": { "close": { "label": "Close", "className": "btn-primary" } } }); } }; var handleUploadFailed = function(message) { message = message || 'Error with file upload'; var result = false; if ($scope.uploadFailed) { result = $scope.uploadFailed({'message': message}); } if (!result) { bootbox.dialog({ "message": message, "title": "Cannot upload file for Dockerfile build", "buttons": { "close": { "label": "Close", "className": "btn-primary" } } }); } }; var handleMissingFile = function() { var result = false; if ($scope.missingFile) { result = $scope.missingFile({}); } if (!result) { bootbox.dialog({ "message": 'A Dockerfile or an archive containing a Dockerfile is required', "title": "Missing Dockerfile", "buttons": { "close": { "label": "Close", "className": "btn-primary" } } }); } }; var startBuild = function(fileId) { $scope.building = true; var repo = $scope.repository; var data = { 'file_id': fileId }; var params = { 'repository': repo.namespace + '/' + repo.name }; ApiService.requestRepoBuild(data, params).then(function(resp) { $scope.building = false; $scope.uploading = false; if ($scope.buildStarted) { $scope.buildStarted({'build': resp}); } }, function(resp) { $scope.building = false; $scope.uploading = false; handleBuildFailed(resp.message); }); }; var conductUpload = function(file, url, fileId, mimeType) { if ($scope.uploadStarted) { $scope.uploadStarted({}); } var request = new XMLHttpRequest(); request.open('PUT', url, true); request.setRequestHeader('Content-Type', mimeType); request.onprogress = function(e) { $scope.$apply(function() { var percentLoaded; if (e.lengthComputable) { $scope.upload_progress = (e.loaded / e.total) * 100; } }); }; request.onerror = function() { $scope.$apply(function() { handleUploadFailed(); }); }; request.onreadystatechange = function() { var state = request.readyState; if (state == 4) { $scope.$apply(function() { startBuild(fileId); $scope.uploading = false; }); return; } }; request.send(file); }; var startFileUpload = function(repo) { $scope.uploading = true; $scope.uploading_progress = 0; var uploader = $('#file-drop')[0]; if (uploader.files.length == 0) { handleMissingFile(); $scope.uploading = false; return; } var file = uploader.files[0]; $scope.upload_file = file.name; var mimeType = file.type || 'application/octet-stream'; var data = { 'mimeType': mimeType }; var getUploadUrl = ApiService.getFiledropUrl(data).then(function(resp) { conductUpload(file, resp.url, resp.file_id, mimeType); }, function() { handleUploadFailed('Could not retrieve upload URL'); }); }; $scope.$watch('internal.hasDockerfile', function(d) { $scope.hasDockerfile = d; }); $scope.$watch('startNow', function() { if ($scope.startNow && $scope.repository && !$scope.uploading && !$scope.building) { startFileUpload(); } }); } }; return directiveDefinitionObject; }); quayApp.directive('locationView', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/location-view.html', replace: false, transclude: true, restrict: 'C', scope: { 'location': '=location' }, controller: function($rootScope, $scope, $element, $http, PingService) { var LOCATIONS = { 'local_us': { 'country': 'US', 'data': 'quay-registry.s3.amazonaws.com', 'title': 'United States' }, 'local_eu': { 'country': 'EU', 'data': 'quay-registry-eu.s3-eu-west-1.amazonaws.com', 'title': 'Europe' }, 's3_us_east_1': { 'country': 'US', 'data': 'quay-registry.s3.amazonaws.com', 'title': 'United States (East)' }, 's3_us_west_1': { 'country': 'US', 'data': 'quay-registry-cali.s3.amazonaws.com', 'title': 'United States (West)' }, 's3_eu_west_1': { 'country': 'EU', 'data': 'quay-registry-eu.s3-eu-west-1.amazonaws.com', 'title': 'Europe' }, 's3_ap_southeast_1': { 'country': 'SG', 'data': 'quay-registry-singapore.s3-ap-southeast-1.amazonaws.com', 'title': 'Singapore' }, 's3_ap_southeast_2': { 'country': 'AU', 'data': 'quay-registry-sydney.s3-ap-southeast-2.amazonaws.com', 'title': 'Australia' }, // 's3_ap_northeast-1': { 'country': 'JP', 'data': 's3-ap-northeast-1.amazonaws.com', 'title': 'Japan' }, // 's3_sa_east1': { 'country': 'BR', 'data': 's3-east-1.amazonaws.com', 'title': 'Sao Paulo' } }; $scope.locationPing = null; $scope.locationPingClass = null; $scope.getLocationTooltip = function(location, ping) { var tip = $scope.getLocationTitle(location) + '
'; if (ping == null) { tip += '(Loading)'; } else if (ping < 0) { tip += '
Note: Could not contact server'; } else { tip += 'Estimated Ping: ' + (ping ? ping + 'ms' : '(Loading)'); } return tip; }; $scope.getLocationTitle = function(location) { if (!LOCATIONS[location]) { return '(Unknown)'; } return 'Image data is located in ' + LOCATIONS[location]['title']; }; $scope.getLocationImage = function(location) { if (!LOCATIONS[location]) { return 'unknown.png'; } return LOCATIONS[location]['country'] + '.png'; }; $scope.getLocationPing = function(location) { var url = 'https://' + LOCATIONS[location]['data'] + '/okay.txt'; PingService.pingUrl($scope, url, function(ping, success, count) { if (count == 3 || !success) { $scope.locationPing = success ? ping : -1; } }); }; $scope.$watch('location', function(location) { if (!location) { return; } $scope.getLocationPing(location); }); $scope.$watch('locationPing', function(locationPing) { if (locationPing == null) { $scope.locationPingClass = null; return; } if (locationPing < 0) { $scope.locationPingClass = 'error'; return; } if (locationPing < 100) { $scope.locationPingClass = 'good'; return; } if (locationPing < 250) { $scope.locationPingClass = 'fair'; return; } if (locationPing < 500) { $scope.locationPingClass = 'barely'; return; } $scope.locationPingClass = 'poor'; }); } }; return directiveDefinitionObject; }); quayApp.directive('tagSpecificImagesView', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/tag-specific-images-view.html', replace: false, transclude: true, restrict: 'C', scope: { 'repository': '=repository', 'tag': '=tag', 'images': '=images', 'imageCutoff': '=imageCutoff' }, controller: function($scope, $element) { $scope.getFirstTextLine = getFirstTextLine; $scope.hasImages = false; $scope.tagSpecificImages = []; $scope.getImageListingClasses = function(image) { var classes = ''; if (image.ancestors.length > 1) { classes += 'child '; } var currentTag = $scope.repository.tags[$scope.tag]; if (image.dbid == currentTag.dbid) { classes += 'tag-image '; } return classes; }; var forAllTagImages = function(tag, callback, opt_cutoff) { if (!tag) { return; } if (!$scope.imageByDBID) { $scope.imageByDBID = []; for (var i = 0; i < $scope.images.length; ++i) { var currentImage = $scope.images[i]; $scope.imageByDBID[currentImage.dbid] = currentImage; } } var tag_image = $scope.imageByDBID[tag.dbid]; if (!tag_image) { return; } callback(tag_image); var ancestors = tag_image.ancestors.split('/').reverse(); for (var i = 0; i < ancestors.length; ++i) { var image = $scope.imageByDBID[ancestors[i]]; if (image) { if (image == opt_cutoff) { return; } callback(image); } } }; var refresh = function() { if (!$scope.repository || !$scope.tag || !$scope.images) { $scope.tagSpecificImages = []; return; } var tag = $scope.repository.tags[$scope.tag]; if (!tag) { $scope.tagSpecificImages = []; return; } var getIdsForTag = function(currentTag) { var ids = {}; forAllTagImages(currentTag, function(image) { ids[image.dbid] = true; }, $scope.imageCutoff); return ids; }; // Remove any IDs that match other tags. var toDelete = getIdsForTag(tag); for (var currentTagName in $scope.repository.tags) { var currentTag = $scope.repository.tags[currentTagName]; if (currentTag != tag) { for (var dbid in getIdsForTag(currentTag)) { delete toDelete[dbid]; } } } // Return the matching list of images. var images = []; for (var i = 0; i < $scope.images.length; ++i) { var image = $scope.images[i]; if (toDelete[image.dbid]) { images.push(image); } } images.sort(function(a, b) { var result = new Date(b.created) - new Date(a.created); if (result != 0) { return result; } return b.dbid - a.dbid; }); $scope.tagSpecificImages = images; }; $scope.$watch('repository', refresh); $scope.$watch('tag', refresh); $scope.$watch('images', refresh); } }; return directiveDefinitionObject; }); quayApp.directive('fallbackSrc', function () { return { restrict: 'A', link: function postLink(scope, element, attributes) { element.bind('error', function() { angular.element(this).attr("src", attributes.fallbackSrc); }); } }; }); // Note: ngBlur is not yet in Angular stable, so we add it manaully here. quayApp.directive('ngBlur', function() { return function( scope, elem, attrs ) { elem.bind('blur', function() { scope.$apply(attrs.ngBlur); }); }; }); quayApp.directive("filePresent", [function () { return { restrict: 'A', scope: { 'filePresent': "=" }, link: function (scope, element, attributes) { element.bind("change", function (changeEvent) { scope.$apply(function() { scope.filePresent = changeEvent.target.files.length > 0; }); }); } } }]); quayApp.directive('ngVisible', function () { return function (scope, element, attr) { scope.$watch(attr.ngVisible, function (visible) { element.css('visibility', visible ? 'visible' : 'hidden'); }); }; }); quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanService', '$http', '$timeout', 'CookieService', 'Features', '$anchorScroll', function($location, $rootScope, Restangular, UserService, PlanService, $http, $timeout, CookieService, Features, $anchorScroll) { // Handle session security. Restangular.setDefaultRequestParams(['post', 'put', 'remove', 'delete'], {'_csrf_token': window.__token || ''}); // Handle session expiration. Restangular.setErrorInterceptor(function(response) { if (response.status == 401 && response.data['error_type'] == 'invalid_token' && response.data['session_required'] !== false) { $('#sessionexpiredModal').modal({}); return false; } if (response.status == 503) { $('#cannotContactService').modal({}); return false; } if (response.status == 500) { document.location = '/500'; return false; } return true; }); // Check if we need to redirect based on a previously chosen plan. var result = PlanService.handleNotedPlan(); // Check to see if we need to show a redirection page. var redirectUrl = CookieService.get('quay.redirectAfterLoad'); CookieService.clear('quay.redirectAfterLoad'); if (!result && redirectUrl && redirectUrl.indexOf(window.location.href) == 0) { window.location = redirectUrl; return; } var changeTab = function(activeTab, opt_timeout) { var checkCount = 0; $timeout(function() { if (checkCount > 5) { return; } checkCount++; $('a[data-toggle="tab"]').each(function(index) { var tabName = this.getAttribute('data-target').substr(1); if (tabName != activeTab) { return; } if (this.clientWidth == 0) { changeTab(activeTab, 500); return; } clickElement(this); }); }, opt_timeout); }; var resetDefaultTab = function() { $timeout(function() { $('a[data-toggle="tab"]').each(function(index) { if (index == 0) { clickElement(this); } }); }); }; $rootScope.$watch('description', function(description) { if (!description) { description = 'Hosted private docker repositories. Includes full user management and history. Free for public repositories.'; } // Note: We set the content of the description tag manually here rather than using Angular binding // because we need the tag to have a default description that is not of the form "{{ description }}", // we read by tools that do not properly invoke the Angular code. $('#descriptionTag').attr('content', description); }); $rootScope.$on('$routeUpdate', function(){ if ($location.search()['tab']) { changeTab($location.search()['tab']); } else { resetDefaultTab(); } }); $rootScope.$on('$routeChangeSuccess', function (event, current, previous) { $rootScope.pageClass = ''; $rootScope.current = current.$$route; if (!current.$$route) { return; } if (current.$$route.title) { $rootScope.title = current.$$route.title; } if (current.$$route.pageClass) { $rootScope.pageClass = current.$$route.pageClass; } if (current.$$route.description) { $rootScope.description = current.$$route.description; } else { $rootScope.description = ''; } $rootScope.fixFooter = !!current.$$route.fixFooter; $anchorScroll(); }); $rootScope.$on('$viewContentLoaded', function(event, current) { var activeTab = $location.search()['tab']; // Setup deep linking of tabs. This will change the search field of the URL whenever a tab // is changed in the UI. $('a[data-toggle="tab"]').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; var newSearch = $.extend($location.search(), {}); if (isDefaultTab) { delete newSearch['tab']; } else { newSearch['tab'] = tabName; } $location.search(newSearch); }); e.preventDefault(); }); if (activeTab) { changeTab(activeTab); } }); var initallyChecked = false; window.__isLoading = function() { if (!initallyChecked) { initallyChecked = true; return true; } return $http.pendingRequests.length > 0; }; }]);