6875 lines
206 KiB
JavaScript
6875 lines
206 KiB
JavaScript
var TEAM_PATTERN = '^[a-zA-Z][a-zA-Z0-9]+$';
|
||
var ROBOT_PATTERN = '^[a-zA-Z][a-zA-Z0-9]{3,29}$';
|
||
var USER_PATTERN = '^[a-z0-9_]{4,30}$';
|
||
|
||
$.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.isEmailAddress = function(val) {
|
||
var emailRegex = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
|
||
return emailRegex.test(val);
|
||
};
|
||
|
||
utilService.escapeHtmlString = function(text) {
|
||
var adjusted = text.replace(/&/g, "&")
|
||
.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('AvatarService', ['Config', '$sanitize', 'md5',
|
||
function(Config, $sanitize, md5) {
|
||
var avatarService = {};
|
||
var cache = {};
|
||
|
||
avatarService.getAvatar = function(hash, opt_size) {
|
||
var size = opt_size || 16;
|
||
switch (Config['AVATAR_KIND']) {
|
||
case 'local':
|
||
return '/avatar/' + hash + '?size=' + size;
|
||
break;
|
||
|
||
case 'gravatar':
|
||
return '//www.gravatar.com/avatar/' + hash + '?d=identicon&size=' + size;
|
||
break;
|
||
}
|
||
};
|
||
|
||
avatarService.computeHash = function(opt_email, opt_name) {
|
||
var email = opt_email || '';
|
||
var name = opt_name || '';
|
||
|
||
var cacheKey = email + ':' + name;
|
||
if (!cacheKey) { return '-'; }
|
||
|
||
if (cache[cacheKey]) {
|
||
return cache[cacheKey];
|
||
}
|
||
|
||
var hash = md5.createHash(email.toString().toLowerCase());
|
||
switch (Config['AVATAR_KIND']) {
|
||
case 'local':
|
||
if (name) {
|
||
hash = name[0] + hash;
|
||
} else if (email) {
|
||
hash = email[0] + hash;
|
||
}
|
||
break;
|
||
}
|
||
|
||
return cache[cacheKey] = hash;
|
||
};
|
||
|
||
return avatarService;
|
||
}]);
|
||
|
||
$provide.factory('TriggerService', ['UtilService', '$sanitize', 'KeyService',
|
||
function(UtilService, $sanitize, KeyService) {
|
||
var triggerService = {};
|
||
|
||
var triggerTypes = {
|
||
'github': {
|
||
'description': function(config) {
|
||
var source = UtilService.textToSafeHtml(config['build_source']);
|
||
var desc = '<i class="fa fa-github fa-lg" style="margin-left: 2px; margin-right: 2px"></i> Push to Github Repository ';
|
||
desc += '<a href="https://github.com/' + source + '" target="_blank">' + source + '</a>';
|
||
desc += '<br>Dockerfile folder: //' + UtilService.textToSafeHtml(config['subdir']);
|
||
return desc;
|
||
},
|
||
|
||
'run_parameters': [
|
||
{
|
||
'title': 'Branch',
|
||
'type': 'option',
|
||
'name': 'branch_name'
|
||
}
|
||
],
|
||
|
||
'get_redirect_url': function(namespace, repository) {
|
||
var redirect_uri = KeyService['githubRedirectUri'] + '/trigger/' +
|
||
namespace + '/' + repository;
|
||
|
||
var authorize_url = KeyService['githubTriggerAuthorizeUrl'];
|
||
var client_id = KeyService['githubTriggerClientId'];
|
||
|
||
return authorize_url + 'client_id=' + client_id +
|
||
'&scope=repo,user:email&redirect_uri=' + redirect_uri;
|
||
}
|
||
}
|
||
}
|
||
|
||
triggerService.getRedirectUrl = function(name, namespace, repository) {
|
||
var type = triggerTypes[name];
|
||
if (!type) {
|
||
return '';
|
||
}
|
||
return type['get_redirect_url'](namespace, repository);
|
||
};
|
||
|
||
triggerService.getDescription = function(name, config) {
|
||
var type = triggerTypes[name];
|
||
if (!type) {
|
||
return 'Unknown';
|
||
}
|
||
return type['description'](config);
|
||
};
|
||
|
||
triggerService.getRunParameters = function(name, config) {
|
||
var type = triggerTypes[name];
|
||
if (!type) {
|
||
return [];
|
||
}
|
||
return type['run_parameters'];
|
||
}
|
||
|
||
return triggerService;
|
||
}]);
|
||
|
||
$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 = {
|
||
'inviter': 'user',
|
||
'username': 'user',
|
||
'user': 'user',
|
||
'email': 'envelope',
|
||
'activating_username': 'user',
|
||
'delegate_user': 'user',
|
||
'delegate_team': 'group',
|
||
'team': 'group',
|
||
'token': 'key',
|
||
'repo': 'hdd-o',
|
||
'robot': 'wrench',
|
||
'tag': 'tag',
|
||
'role': 'th-large',
|
||
'original_role': 'th-large',
|
||
'application_name': 'cloud',
|
||
'image': 'archive',
|
||
'original_image': 'archive',
|
||
'client_id': 'chain'
|
||
};
|
||
|
||
var filters = {
|
||
'obj': function(value) {
|
||
if (!value) { return []; }
|
||
return Object.getOwnPropertyNames(value);
|
||
},
|
||
|
||
'updated_tags': function(value) {
|
||
if (!value) { return []; }
|
||
return Object.getOwnPropertyNames(value);
|
||
}
|
||
};
|
||
|
||
var description = value_or_func;
|
||
if (typeof description != 'string') {
|
||
description = description(metadata);
|
||
}
|
||
|
||
for (var key in metadata) {
|
||
if (metadata.hasOwnProperty(key)) {
|
||
var value = metadata[key] != null ? metadata[key] : '(Unknown)';
|
||
if (filters[key]) {
|
||
value = filters[key](value);
|
||
}
|
||
|
||
if (Array.isArray(value)) {
|
||
value = value.join(', ');
|
||
}
|
||
|
||
value = value.toString();
|
||
|
||
if (key.indexOf('image') >= 0) {
|
||
value = value.substr(0, 12);
|
||
}
|
||
|
||
var safe = UtilService.escapeHtmlString(value);
|
||
var markedDown = getMarkedDown(value);
|
||
markedDown = markedDown.substr('<p>'.length, markedDown.length - '<p></p>'.length);
|
||
|
||
var icon = fieldIcons[key];
|
||
if (icon) {
|
||
markedDown = '<i class="fa fa-' + icon + '"></i>' + markedDown;
|
||
}
|
||
|
||
description = description.replace('{' + key + '}', '<code title="' + safe + '">' + markedDown + '</code>');
|
||
}
|
||
}
|
||
return $sce.trustAsHtml(description.replace('\n', '<br>'));
|
||
};
|
||
|
||
return stringBuilderService;
|
||
}]);
|
||
|
||
|
||
$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);
|
||
|
||
// Build the path, adjusted with the inline parameters.
|
||
var used = {};
|
||
var url = '';
|
||
for (var i = 0; i < path.length; ++i) {
|
||
var c = path[i];
|
||
if (c == '{') {
|
||
var end = path.indexOf('}', i);
|
||
var varName = path.substr(i + 1, end - i - 1);
|
||
|
||
if (!parameters[varName]) {
|
||
throw new Error('Missing parameter: ' + varName);
|
||
}
|
||
|
||
used[varName] = true;
|
||
url += parameters[varName];
|
||
i = end;
|
||
continue;
|
||
}
|
||
|
||
url += c;
|
||
}
|
||
|
||
// Append any query parameters.
|
||
var isFirst = true;
|
||
for (var paramName in parameters) {
|
||
if (!parameters.hasOwnProperty(paramName)) { continue; }
|
||
if (used[paramName]) { continue; }
|
||
|
||
var value = parameters[paramName];
|
||
if (value) {
|
||
url += isFirst ? '?' : '&';
|
||
url += paramName + '=' + encodeURIComponent(value)
|
||
isFirst = false;
|
||
}
|
||
}
|
||
|
||
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:' +
|
||
'<form style="margin-top: 10px" action="javascript:void(0)">' +
|
||
'<input id="freshPassword" class="form-control" type="password" placeholder="Current Password">' +
|
||
'</form>',
|
||
"title": 'Please Verify',
|
||
"buttons": {
|
||
"verify": {
|
||
"label": "Verify",
|
||
"className": "btn-success",
|
||
"callback": verifyNow
|
||
},
|
||
"close": {
|
||
"label": "Cancel",
|
||
"className": "btn-default",
|
||
"callback": function() {
|
||
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', 'Features', function(Config, Features) {
|
||
var externalNotificationData = {};
|
||
|
||
var events = [
|
||
{
|
||
'id': 'repo_push',
|
||
'title': 'Push to Repository',
|
||
'icon': 'fa-upload'
|
||
}
|
||
];
|
||
|
||
if (Features.BUILD_SUPPORT) {
|
||
var buildEvents = [
|
||
{
|
||
'id': 'build_queued',
|
||
'title': 'Dockerfile Build Queued',
|
||
'icon': 'fa-tasks'
|
||
},
|
||
{
|
||
'id': 'build_start',
|
||
'title': 'Dockerfile Build Started',
|
||
'icon': 'fa-circle-o-notch'
|
||
},
|
||
{
|
||
'id': 'build_success',
|
||
'title': 'Dockerfile Build Successfully Completed',
|
||
'icon': 'fa-check-circle-o'
|
||
},
|
||
{
|
||
'id': 'build_failure',
|
||
'title': 'Dockerfile Build Failed',
|
||
'icon': 'fa-times-circle-o'
|
||
}];
|
||
|
||
for (var i = 0; i < buildEvents.length; ++i) {
|
||
events.push(buildEvents[i]);
|
||
}
|
||
}
|
||
|
||
var methods = [
|
||
{
|
||
'id': 'quay_notification',
|
||
'title': Config.REGISTRY_TITLE + ' Notification',
|
||
'icon': 'quay-icon',
|
||
'fields': [
|
||
{
|
||
'name': 'target',
|
||
'type': 'entity',
|
||
'title': 'Recipient'
|
||
}
|
||
]
|
||
},
|
||
{
|
||
'id': 'email',
|
||
'title': 'E-mail',
|
||
'icon': 'fa-envelope',
|
||
'fields': [
|
||
{
|
||
'name': 'email',
|
||
'type': 'email',
|
||
'title': 'E-mail address'
|
||
}
|
||
],
|
||
'enabled': Features.MAILING
|
||
},
|
||
{
|
||
'id': 'webhook',
|
||
'title': 'Webhook POST',
|
||
'icon': 'fa-link',
|
||
'fields': [
|
||
{
|
||
'name': 'url',
|
||
'type': 'url',
|
||
'title': 'Webhook URL'
|
||
}
|
||
]
|
||
},
|
||
{
|
||
'id': 'flowdock',
|
||
'title': 'Flowdock Team Notification',
|
||
'icon': 'flowdock-icon',
|
||
'fields': [
|
||
{
|
||
'name': 'flow_api_token',
|
||
'type': 'string',
|
||
'title': 'Flow API Token',
|
||
'help_url': 'https://www.flowdock.com/account/tokens'
|
||
}
|
||
]
|
||
},
|
||
{
|
||
'id': 'hipchat',
|
||
'title': 'HipChat Room Notification',
|
||
'icon': 'hipchat-icon',
|
||
'fields': [
|
||
{
|
||
'name': 'room_id',
|
||
'type': 'string',
|
||
'title': 'Room ID #'
|
||
},
|
||
{
|
||
'name': 'notification_token',
|
||
'type': 'string',
|
||
'title': 'Room Notification Token',
|
||
'help_url': 'https://hipchat.com/rooms/tokens/{room_id}'
|
||
}
|
||
]
|
||
},
|
||
{
|
||
'id': 'slack',
|
||
'title': 'Slack Room Notification',
|
||
'icon': 'slack-icon',
|
||
'fields': [
|
||
{
|
||
'name': 'url',
|
||
'type': 'regex',
|
||
'title': 'Webhook URL',
|
||
'regex': '^https://hooks\\.slack\\.com/services/[A-Z0-9]+/[A-Z0-9]+/[a-zA-Z0-9]+$',
|
||
'help_url': 'https://slack.com/services/new/incoming-webhook',
|
||
'placeholder': 'https://hooks.slack.com/service/{some}/{token}/{here}'
|
||
}
|
||
]
|
||
}
|
||
];
|
||
|
||
var methodMap = {};
|
||
var eventMap = {};
|
||
|
||
for (var i = 0; i < methods.length; ++i) {
|
||
methodMap[methods[i].id] = methods[i];
|
||
}
|
||
|
||
for (var i = 0; i < events.length; ++i) {
|
||
eventMap[events[i].id] = events[i];
|
||
}
|
||
|
||
externalNotificationData.getSupportedEvents = function() {
|
||
return events;
|
||
};
|
||
|
||
externalNotificationData.getSupportedMethods = function() {
|
||
var filtered = [];
|
||
for (var i = 0; i < methods.length; ++i) {
|
||
if (methods[i].enabled !== false) {
|
||
filtered.push(methods[i]);
|
||
}
|
||
}
|
||
return filtered;
|
||
};
|
||
|
||
externalNotificationData.getEventInfo = function(event) {
|
||
return eventMap[event];
|
||
};
|
||
|
||
externalNotificationData.getMethodInfo = function(method) {
|
||
return methodMap[method];
|
||
};
|
||
|
||
return externalNotificationData;
|
||
}]);
|
||
|
||
$provide.factory('NotificationService', ['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', 'PlanService', 'UserService', 'Config', '$location',
|
||
function($rootScope, $interval, UserService, ApiService, StringBuilderService, PlanService, UserService, Config, $location) {
|
||
var notificationService = {
|
||
'user': null,
|
||
'notifications': [],
|
||
'notificationClasses': [],
|
||
'notificationSummaries': [],
|
||
'additionalNotifications': false
|
||
};
|
||
|
||
var pollTimerHandle = null;
|
||
|
||
var notificationKinds = {
|
||
'test_notification': {
|
||
'level': 'primary',
|
||
'message': 'This notification is a long message for testing: {obj}',
|
||
'page': '/about/',
|
||
'dismissable': true
|
||
},
|
||
'org_team_invite': {
|
||
'level': 'primary',
|
||
'message': '{inviter} is inviting you to join team {team} under organization {org}',
|
||
'actions': [
|
||
{
|
||
'title': 'Join team',
|
||
'kind': 'primary',
|
||
'handler': function(notification) {
|
||
window.location = '/confirminvite?code=' + notification.metadata['code'];
|
||
}
|
||
},
|
||
{
|
||
'title': 'Decline',
|
||
'kind': 'default',
|
||
'handler': function(notification) {
|
||
ApiService.declineOrganizationTeamInvite(null, {'code': notification.metadata['code']}).then(function() {
|
||
notificationService.update();
|
||
});
|
||
}
|
||
}
|
||
]
|
||
},
|
||
'password_required': {
|
||
'level': 'error',
|
||
'message': 'In order to begin pushing and pulling repositories, a password must be set for your account',
|
||
'page': '/user?tab=password'
|
||
},
|
||
'over_private_usage': {
|
||
'level': 'error',
|
||
'message': 'Namespace {namespace} is over its allowed private repository count. ' +
|
||
'<br><br>Please upgrade your plan to avoid disruptions in service.',
|
||
'page': function(metadata) {
|
||
var organization = UserService.getOrganization(metadata['namespace']);
|
||
if (organization) {
|
||
return '/organization/' + metadata['namespace'] + '/admin';
|
||
} else {
|
||
return '/user';
|
||
}
|
||
}
|
||
},
|
||
'expiring_license': {
|
||
'level': 'error',
|
||
'message': 'Your license will expire at: {expires_at} ' +
|
||
'<br><br>Please contact support to purchase a new license.',
|
||
'page': '/contact/'
|
||
},
|
||
'maintenance': {
|
||
'level': 'warning',
|
||
'message': 'We will be down for schedule maintenance from {from_date} to {to_date} ' +
|
||
'for {reason}. We are sorry about any inconvenience.',
|
||
'page': 'http://status.quay.io/'
|
||
},
|
||
'repo_push': {
|
||
'level': 'info',
|
||
'message': function(metadata) {
|
||
if (metadata.updated_tags && Object.getOwnPropertyNames(metadata.updated_tags).length) {
|
||
return 'Repository {repository} has been pushed with the following tags updated: {updated_tags}';
|
||
} else {
|
||
return 'Repository {repository} fhas been pushed';
|
||
}
|
||
},
|
||
'page': function(metadata) {
|
||
return '/repository/' + metadata.repository;
|
||
},
|
||
'dismissable': true
|
||
},
|
||
'build_queued': {
|
||
'level': 'info',
|
||
'message': 'A build has been queued for repository {repository}',
|
||
'page': function(metadata) {
|
||
return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
|
||
},
|
||
'dismissable': true
|
||
},
|
||
'build_start': {
|
||
'level': 'info',
|
||
'message': 'A build has been started for repository {repository}',
|
||
'page': function(metadata) {
|
||
return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
|
||
},
|
||
'dismissable': true
|
||
},
|
||
'build_success': {
|
||
'level': 'info',
|
||
'message': 'A build has succeeded for repository {repository}',
|
||
'page': function(metadata) {
|
||
return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
|
||
},
|
||
'dismissable': true
|
||
},
|
||
'build_failure': {
|
||
'level': 'error',
|
||
'message': 'A build has failed for repository {repository}',
|
||
'page': function(metadata) {
|
||
return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
|
||
},
|
||
'dismissable': true
|
||
}
|
||
};
|
||
|
||
notificationService.dismissNotification = function(notification) {
|
||
notification.dismissed = true;
|
||
var params = {
|
||
'uuid': notification.id
|
||
};
|
||
|
||
ApiService.updateUserNotification(notification, params, function() {
|
||
notificationService.update();
|
||
}, ApiService.errorDisplay('Could not update notification'));
|
||
|
||
var index = $.inArray(notification, notificationService.notifications);
|
||
if (index >= 0) {
|
||
notificationService.notifications.splice(index, 1);
|
||
}
|
||
};
|
||
|
||
notificationService.getActions = function(notification) {
|
||
var kindInfo = notificationKinds[notification['kind']];
|
||
if (!kindInfo) {
|
||
return [];
|
||
}
|
||
|
||
return kindInfo['actions'] || [];
|
||
};
|
||
|
||
notificationService.canDismiss = function(notification) {
|
||
var kindInfo = notificationKinds[notification['kind']];
|
||
if (!kindInfo) {
|
||
return false;
|
||
}
|
||
return !!kindInfo['dismissable'];
|
||
};
|
||
|
||
notificationService.getPage = function(notification) {
|
||
var kindInfo = notificationKinds[notification['kind']];
|
||
if (!kindInfo) {
|
||
return null;
|
||
}
|
||
|
||
var page = kindInfo['page'];
|
||
if (page != null && typeof page != 'string') {
|
||
page = page(notification['metadata']);
|
||
}
|
||
return page || '';
|
||
};
|
||
|
||
notificationService.getMessage = function(notification) {
|
||
var kindInfo = notificationKinds[notification['kind']];
|
||
if (!kindInfo) {
|
||
return '(Unknown notification kind: ' + notification['kind'] + ')';
|
||
}
|
||
return StringBuilderService.buildString(kindInfo['message'], notification['metadata']);
|
||
};
|
||
|
||
notificationService.getClass = function(notification) {
|
||
var kindInfo = notificationKinds[notification['kind']];
|
||
if (!kindInfo) {
|
||
return 'notification-info';
|
||
}
|
||
return 'notification-' + kindInfo['level'];
|
||
};
|
||
|
||
notificationService.getClasses = function(notifications) {
|
||
var classes = [];
|
||
for (var i = 0; i < notifications.length; ++i) {
|
||
var notification = notifications[i];
|
||
classes.push(notificationService.getClass(notification));
|
||
}
|
||
return classes.join(' ');
|
||
};
|
||
|
||
notificationService.update = function() {
|
||
var user = UserService.currentUser();
|
||
if (!user || user.anonymous) {
|
||
return;
|
||
}
|
||
|
||
ApiService.listUserNotifications().then(function(resp) {
|
||
notificationService.notifications = resp['notifications'];
|
||
notificationService.additionalNotifications = resp['additional'];
|
||
notificationService.notificationClasses = notificationService.getClasses(notificationService.notifications);
|
||
});
|
||
};
|
||
|
||
notificationService.reset = function() {
|
||
$interval.cancel(pollTimerHandle);
|
||
pollTimerHandle = $interval(notificationService.update, 5 * 60 * 1000 /* five minutes */);
|
||
};
|
||
|
||
// Watch for plan changes and update.
|
||
PlanService.registerListener(this, function(plan) {
|
||
notificationService.reset();
|
||
notificationService.update();
|
||
});
|
||
|
||
// Watch for user changes and update.
|
||
$rootScope.$watch(function() { return UserService.currentUser(); }, function(currentUser) {
|
||
notificationService.reset();
|
||
notificationService.update();
|
||
});
|
||
|
||
return notificationService;
|
||
}]);
|
||
|
||
$provide.factory('OAuthService', ['$location', 'Config', function($location, Config) {
|
||
var oauthService = {};
|
||
oauthService.SCOPES = window.__auth_scopes;
|
||
return oauthService;
|
||
}]);
|
||
|
||
$provide.factory('KeyService', ['$location', 'Config', function($location, Config) {
|
||
var keyService = {}
|
||
var oauth = window.__oauth;
|
||
|
||
keyService['stripePublishableKey'] = Config['STRIPE_PUBLISHABLE_KEY'];
|
||
|
||
keyService['githubTriggerClientId'] = oauth['GITHUB_TRIGGER_CONFIG']['CLIENT_ID'];
|
||
keyService['githubLoginClientId'] = oauth['GITHUB_LOGIN_CONFIG']['CLIENT_ID'];
|
||
keyService['googleLoginClientId'] = oauth['GOOGLE_LOGIN_CONFIG']['CLIENT_ID'];
|
||
|
||
keyService['githubRedirectUri'] = Config.getUrl('/oauth2/github/callback');
|
||
keyService['googleRedirectUri'] = Config.getUrl('/oauth2/google/callback');
|
||
|
||
keyService['githubLoginUrl'] = oauth['GITHUB_LOGIN_CONFIG']['AUTHORIZE_ENDPOINT'];
|
||
keyService['googleLoginUrl'] = oauth['GOOGLE_LOGIN_CONFIG']['AUTHORIZE_ENDPOINT'];
|
||
|
||
keyService['githubEndpoint'] = oauth['GITHUB_LOGIN_CONFIG']['GITHUB_ENDPOINT'];
|
||
|
||
keyService['githubTriggerEndpoint'] = oauth['GITHUB_TRIGGER_CONFIG']['GITHUB_ENDPOINT'];
|
||
keyService['githubTriggerAuthorizeUrl'] = oauth['GITHUB_TRIGGER_CONFIG']['AUTHORIZE_ENDPOINT'];
|
||
|
||
keyService['githubLoginScope'] = 'user:email';
|
||
keyService['googleLoginScope'] = 'openid email';
|
||
|
||
keyService.isEnterprise = function(service) {
|
||
switch (service) {
|
||
case 'github':
|
||
return keyService['githubLoginUrl'].indexOf('https://github.com/') < 0;
|
||
|
||
case 'github-trigger':
|
||
return keyService['githubTriggerAuthorizeUrl'].indexOf('https://github.com/') < 0;
|
||
}
|
||
|
||
return false;
|
||
};
|
||
|
||
keyService.getExternalLoginUrl = function(service, action) {
|
||
var state_clause = '';
|
||
if (Config.MIXPANEL_KEY && window.mixpanel) {
|
||
if (mixpanel.get_distinct_id !== undefined) {
|
||
state_clause = "&state=" + encodeURIComponent(mixpanel.get_distinct_id());
|
||
}
|
||
}
|
||
|
||
var client_id = keyService[service + 'LoginClientId'];
|
||
var scope = keyService[service + 'LoginScope'];
|
||
var redirect_uri = keyService[service + 'RedirectUri'];
|
||
if (action == 'attach') {
|
||
redirect_uri += '/attach';
|
||
}
|
||
|
||
var url = keyService[service + 'LoginUrl'] + 'client_id=' + client_id + '&scope=' + scope +
|
||
'&redirect_uri=' + redirect_uri + state_clause;
|
||
|
||
return url;
|
||
};
|
||
|
||
return keyService;
|
||
}]);
|
||
|
||
$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', controller: SignInCtrl, reloadOnSearch: false}).
|
||
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('/confirminvite', {title: 'Confirm Invite', templateUrl: '/static/partials/confirm-invite.html', controller: ConfirmInviteCtrl, reloadOnSearch: false}).
|
||
|
||
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('ngIfMedia', function ($animate) {
|
||
return {
|
||
transclude: 'element',
|
||
priority: 600,
|
||
terminal: true,
|
||
restrict: 'A',
|
||
link: buildConditionalLinker($animate, 'ngIfMedia', function(value) {
|
||
return window.matchMedia(value).matches;
|
||
})
|
||
};
|
||
});
|
||
|
||
|
||
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',
|
||
'showAvatar': '@showAvatar',
|
||
'avatarSize': '@avatarSize'
|
||
},
|
||
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('focusablePopoverContent', ['$timeout', '$popover', function ($timeout, $popover) {
|
||
return {
|
||
restrict: "A",
|
||
link: function (scope, element, attrs) {
|
||
$body = $('body');
|
||
var hide = function() {
|
||
$body.off('click');
|
||
|
||
if (!scope) { return; }
|
||
scope.$apply(function() {
|
||
if (!scope || !$scope.$hide) { return; }
|
||
scope.$hide();
|
||
});
|
||
};
|
||
|
||
scope.$on('$destroy', function() {
|
||
$body.off('click');
|
||
});
|
||
|
||
$timeout(function() {
|
||
$body.on('click', function(evt) {
|
||
var target = evt.target;
|
||
var isPanelMember = $(element).has(target).length > 0 || target == element;
|
||
if (!isPanelMember) {
|
||
hide();
|
||
}
|
||
});
|
||
|
||
$(element).find('input').focus();
|
||
}, 100);
|
||
}
|
||
};
|
||
}]);
|
||
|
||
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',
|
||
|
||
'inviteCode': '=inviteCode',
|
||
|
||
'signInStarted': '&signInStarted',
|
||
'signedIn': '&signedIn',
|
||
'userRegistered': '&userRegistered'
|
||
},
|
||
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) {
|
||
$scope.sendRecovery = function() {
|
||
$scope.sendingRecovery = true;
|
||
|
||
ApiService.requestRecoveryEmail($scope.recovery).then(function() {
|
||
$scope.invalidRecovery = false;
|
||
$scope.errorMessage = '';
|
||
$scope.sent = true;
|
||
$scope.sendingRecovery = false;
|
||
}, function(resp) {
|
||
$scope.invalidRecovery = true;
|
||
$scope.errorMessage = ApiService.getErrorMessage(resp, 'Cannot send recovery email');
|
||
$scope.sent = false;
|
||
$scope.sendingRecovery = false;
|
||
});
|
||
};
|
||
|
||
$scope.handleUserRegistered = function(username) {
|
||
$scope.userRegistered({'username': username});
|
||
};
|
||
|
||
$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.signingIn = false;
|
||
$scope.isEnterprise = KeyService.isEnterprise;
|
||
|
||
$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.
|
||
$scope.signingIn = true;
|
||
$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.signingIn = false;
|
||
|
||
$scope.markStarted = function() {
|
||
$scope.signingIn = true;
|
||
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.signingIn = false;
|
||
$scope.needsEmailVerification = false;
|
||
$scope.invalidCredentials = false;
|
||
|
||
if ($scope.signedIn != null) {
|
||
$scope.signedIn();
|
||
}
|
||
|
||
// Load the newly created user.
|
||
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() {
|
||
var redirectUrl = $scope.redirectUrl;
|
||
if (redirectUrl == $location.path() || redirectUrl == null) {
|
||
return;
|
||
}
|
||
window.location = (redirectUrl ? redirectUrl : '/');
|
||
}, 500);
|
||
}, function(result) {
|
||
$scope.signingIn = false;
|
||
|
||
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: {
|
||
'inviteCode': '=inviteCode',
|
||
|
||
'userRegistered': '&userRegistered'
|
||
},
|
||
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;
|
||
|
||
if ($scope.inviteCode) {
|
||
$scope.newUser['invite_code'] = $scope.inviteCode;
|
||
}
|
||
|
||
ApiService.createNewUser($scope.newUser).then(function(resp) {
|
||
$scope.registering = false;
|
||
$scope.awaitingConfirmation = !!resp['awaiting_verification'];
|
||
|
||
if (Config.MIXPANEL_KEY) {
|
||
mixpanel.alias($scope.newUser.username);
|
||
}
|
||
|
||
$scope.userRegistered({'username': $scope.newUser.username});
|
||
|
||
if (!$scope.awaitingConfirmation) {
|
||
document.location = '/';
|
||
}
|
||
}, 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['SERVER_HOSTNAME']] = {
|
||
"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('regex', function() {
|
||
return function(input, regex) {
|
||
if (!regex) { return []; }
|
||
|
||
try {
|
||
var patt = new RegExp(regex);
|
||
} catch (ex) {
|
||
return [];
|
||
}
|
||
|
||
var out = [];
|
||
for (var i = 0; i < input.length; ++i){
|
||
var m = input[i].match(patt);
|
||
if (m && m[0].length == input[i].length) {
|
||
out.push(input[i]);
|
||
}
|
||
}
|
||
return out;
|
||
};
|
||
});
|
||
|
||
|
||
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',
|
||
'allLogs': '@allLogs'
|
||
},
|
||
controller: function($scope, $element, $sce, Restangular, ApiService, TriggerService,
|
||
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}',
|
||
'repo_verb': function(metadata) {
|
||
var prefix = '';
|
||
if (metadata.verb == 'squash') {
|
||
prefix = 'Pull of squashed tag {tag}'
|
||
}
|
||
|
||
if (metadata.token) {
|
||
prefix += ' via token {token}';
|
||
} else if (metadata.username) {
|
||
prefix += ' by {username}';
|
||
} else {
|
||
prefix += ' by {_ip}';
|
||
}
|
||
|
||
return prefix;
|
||
},
|
||
'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 = TriggerService.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_invite_team_member': function(metadata) {
|
||
if (metadata.user) {
|
||
return 'Invite {user} to team {team}';
|
||
} else {
|
||
return 'Invite {email} to team {team}';
|
||
}
|
||
},
|
||
'org_delete_team_member_invite': function(metadata) {
|
||
if (metadata.user) {
|
||
return 'Rescind invite of {user} to team {team}';
|
||
} else {
|
||
return 'Rescind invite of {email} to team {team}';
|
||
}
|
||
},
|
||
|
||
'org_team_member_invite_accepted': 'User {member}, invited by {inviter}, joined team {team}',
|
||
'org_team_member_invite_declined': 'User {member}, invited by {inviter}, declined to join 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 = TriggerService.getDescription(
|
||
metadata['service'], metadata['config']);
|
||
return 'Setup build trigger - ' + triggerDescription;
|
||
},
|
||
'delete_repo_trigger': function(metadata) {
|
||
var triggerDescription = TriggerService.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',
|
||
'repo_verb': 'Pull Repo Verb',
|
||
'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_invite_team_member': 'Invite team member',
|
||
'org_delete_team_member_invite': 'Rescind team member invitation',
|
||
'org_remove_team_member': 'Remove team member',
|
||
'org_team_member_invite_accepted': 'Team invite accepted',
|
||
'org_team_member_invite_declined': 'Team invite declined',
|
||
'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 || $scope.allLogs;
|
||
|
||
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');
|
||
}
|
||
|
||
if ($scope.allLogs) {
|
||
url = getRestUrl('superuser', 'logs')
|
||
}
|
||
|
||
url += '?starttime=' + encodeURIComponent(getDateString($scope.logStartDate));
|
||
url += '&endtime=' + encodeURIComponent(getDateString($scope.logEndDate));
|
||
|
||
if ($scope.performer) {
|
||
url += '&performer=' + encodeURIComponent($scope.performer.name);
|
||
}
|
||
|
||
var loadLogs = Restangular.one(url);
|
||
loadLogs.customGET().then(function(resp) {
|
||
$scope.logsPath = '/api/v1/' + url;
|
||
|
||
if (!$scope.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 = '<div class="repo-mini-listing">';
|
||
template += '<i class="fa fa-hdd-o fa-lg"></i>'
|
||
template += '<span class="name">' + datum.repo.namespace +'/' + datum.repo.name + '</span>'
|
||
if (datum.repo.description) {
|
||
template += '<span class="description">' + getFirstTextLine(datum.repo.description) + '</span>'
|
||
}
|
||
|
||
template += '</div>'
|
||
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, Config) {
|
||
$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 "";
|
||
};
|
||
|
||
$scope.getEnterpriseLogo = function() {
|
||
if (!Config.ENTERPRISE_LOGO_URL) {
|
||
return '/static/img/quay-logo.png';
|
||
}
|
||
|
||
return Config.ENTERPRISE_LOGO_URL;
|
||
};
|
||
}
|
||
};
|
||
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',
|
||
'emailSelected': '&emailSelected',
|
||
|
||
// 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',
|
||
|
||
// Whether e-mail addresses are allowed.
|
||
'allowEmails': '=allowEmails',
|
||
'emailMessage': '@emailMessage',
|
||
|
||
// True if the menu should pull right.
|
||
'pullRight': '@pullRight'
|
||
},
|
||
controller: function($rootScope, $scope, $element, Restangular, UserService, ApiService, UtilService, 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 (UtilService.isEmailAddress(val)) {
|
||
if ($scope.allowEmails) {
|
||
return '<div class="tt-message">' + $scope.emailMessage + '</div>';
|
||
} else {
|
||
return '<div class="tt-empty">A ' + Config.REGISTRY_TITLE_SHORT + ' username (not an e-mail address) must be specified</div>';
|
||
}
|
||
}
|
||
|
||
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 '<div class="tt-empty">No matching ' + Config.REGISTRY_TITLE_SHORT + ' ' + class_string + ' found</div>';
|
||
}
|
||
|
||
return null;
|
||
},
|
||
'suggestion': function (datum) {
|
||
template = '<div class="entity-mini-listing">';
|
||
if (datum.entity.kind == 'user' && !datum.entity.is_robot) {
|
||
template += '<i class="fa fa-user fa-lg"></i>';
|
||
} else if (datum.entity.kind == 'user' && datum.entity.is_robot) {
|
||
template += '<i class="fa fa-wrench fa-lg"></i>';
|
||
} else if (datum.entity.kind == 'team') {
|
||
template += '<i class="fa fa-group fa-lg"></i>';
|
||
} else if (datum.entity.kind == 'org') {
|
||
template += '<i class="fa">' + AvatarService.getAvatar(datum.entity.avatar, 16) + '</i>';
|
||
}
|
||
|
||
template += '<span class="name">' + datum.value + '</span>';
|
||
|
||
if (datum.entity.is_org_member === false && datum.entity.kind == 'user') {
|
||
template += '<i class="fa fa-exclamation-triangle" title="User is outside the organization"></i>';
|
||
}
|
||
|
||
template += '</div>';
|
||
return template;
|
||
}}
|
||
});
|
||
|
||
$(input).on('keypress', function(e) {
|
||
var val = $(input).val();
|
||
var code = e.keyCode || e.which;
|
||
if (code == 13 && $scope.allowEmails && UtilService.isEmailAddress(val)) {
|
||
$scope.$apply(function() {
|
||
$scope.emailSelected({'email': val});
|
||
});
|
||
}
|
||
});
|
||
|
||
$(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 });
|
||
}
|
||
|
||
$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.isInternalError = function() {
|
||
var entry = $scope.entries[$scope.entries.length - 1];
|
||
return entry && entry.data && entry.data['internal_error'];
|
||
};
|
||
|
||
$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, KeyService, TriggerService) {
|
||
$scope.KeyService = KeyService;
|
||
$scope.TriggerService = TriggerService;
|
||
}
|
||
};
|
||
return directiveDefinitionObject;
|
||
});
|
||
|
||
|
||
quayApp.directive('stepView', function ($compile) {
|
||
var directiveDefinitionObject = {
|
||
priority: 0,
|
||
templateUrl: '/static/directives/step-view.html',
|
||
replace: true,
|
||
transclude: true,
|
||
restrict: 'C',
|
||
scope: {
|
||
'nextStepCounter': '=nextStepCounter',
|
||
'currentStepValid': '=currentStepValid',
|
||
|
||
'stepsCompleted': '&stepsCompleted'
|
||
},
|
||
controller: function($scope, $element, $rootScope) {
|
||
this.currentStepIndex = -1;
|
||
this.steps = [];
|
||
this.watcher = null;
|
||
|
||
this.getCurrentStep = function() {
|
||
return this.steps[this.currentStepIndex];
|
||
};
|
||
|
||
this.reset = function() {
|
||
this.currentStepIndex = -1;
|
||
for (var i = 0; i < this.steps.length; ++i) {
|
||
this.steps[i].element.hide();
|
||
}
|
||
|
||
$scope.currentStepValid = false;
|
||
};
|
||
|
||
this.next = function() {
|
||
if (this.currentStepIndex >= 0) {
|
||
var currentStep = this.getCurrentStep();
|
||
if (!currentStep || !currentStep.scope) { return; }
|
||
|
||
if (!currentStep.scope.completeCondition) {
|
||
return;
|
||
}
|
||
|
||
currentStep.element.hide();
|
||
|
||
if (this.unwatch) {
|
||
this.unwatch();
|
||
this.unwatch = null;
|
||
}
|
||
}
|
||
|
||
this.currentStepIndex++;
|
||
|
||
if (this.currentStepIndex < this.steps.length) {
|
||
var currentStep = this.getCurrentStep();
|
||
currentStep.element.show();
|
||
currentStep.scope.load()
|
||
|
||
this.unwatch = currentStep.scope.$watch('completeCondition', function(cc) {
|
||
$scope.currentStepValid = !!cc;
|
||
});
|
||
} else {
|
||
$scope.stepsCompleted();
|
||
}
|
||
};
|
||
|
||
this.register = function(scope, element) {
|
||
element.hide();
|
||
|
||
this.steps.push({
|
||
'scope': scope,
|
||
'element': element
|
||
});
|
||
};
|
||
|
||
var that = this;
|
||
$scope.$watch('nextStepCounter', function(nsc) {
|
||
if (nsc >= 0) {
|
||
that.next();
|
||
} else {
|
||
that.reset();
|
||
}
|
||
});
|
||
}
|
||
};
|
||
return directiveDefinitionObject;
|
||
});
|
||
|
||
|
||
quayApp.directive('stepViewStep', function () {
|
||
var directiveDefinitionObject = {
|
||
priority: 1,
|
||
require: '^stepView',
|
||
templateUrl: '/static/directives/step-view-step.html',
|
||
replace: false,
|
||
transclude: true,
|
||
restrict: 'C',
|
||
scope: {
|
||
'completeCondition': '=completeCondition',
|
||
'loadCallback': '&loadCallback',
|
||
'loadMessage': '@loadMessage'
|
||
},
|
||
link: function(scope, element, attrs, controller) {
|
||
controller.register(scope, element);
|
||
},
|
||
controller: function($scope, $element) {
|
||
$scope.load = function() {
|
||
$scope.loading = true;
|
||
$scope.loadCallback({'callback': function() {
|
||
$scope.loading = false;
|
||
}});
|
||
};
|
||
}
|
||
};
|
||
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('manualTriggerBuildDialog', function () {
|
||
var directiveDefinitionObject = {
|
||
templateUrl: '/static/directives/manual-trigger-build-dialog.html',
|
||
replace: false,
|
||
transclude: false,
|
||
restrict: 'C',
|
||
scope: {
|
||
'repository': '=repository',
|
||
'counter': '=counter',
|
||
'trigger': '=trigger',
|
||
'startBuild': '&startBuild'
|
||
},
|
||
controller: function($scope, $element, ApiService, TriggerService) {
|
||
$scope.parameters = {};
|
||
$scope.fieldOptions = {};
|
||
|
||
$scope.startTrigger = function() {
|
||
$('#startTriggerDialog').modal('hide');
|
||
$scope.startBuild({
|
||
'trigger': $scope.trigger,
|
||
'parameters': $scope.parameters
|
||
});
|
||
};
|
||
|
||
$scope.show = function() {
|
||
$scope.parameters = {};
|
||
$scope.fieldOptions = {};
|
||
|
||
var parameters = TriggerService.getRunParameters($scope.trigger.service);
|
||
for (var i = 0; i < parameters.length; ++i) {
|
||
var parameter = parameters[i];
|
||
if (parameter['type'] == 'option') {
|
||
// Load the values for this parameter.
|
||
var params = {
|
||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||
'trigger_uuid': $scope.trigger.id,
|
||
'field_name': parameter['name']
|
||
};
|
||
|
||
ApiService.listTriggerFieldValues(null, params).then(function(resp) {
|
||
$scope.fieldOptions[parameter['name']] = resp['values'];
|
||
});
|
||
}
|
||
}
|
||
$scope.runParameters = parameters;
|
||
|
||
$('#startTriggerDialog').modal('show');
|
||
};
|
||
|
||
$scope.$watch('counter', function(counter) {
|
||
if (counter) {
|
||
$scope.show();
|
||
}
|
||
});
|
||
}
|
||
};
|
||
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.state = {};
|
||
$scope.nextStepCounter = -1;
|
||
$scope.currentView = 'config';
|
||
|
||
$scope.show = function() {
|
||
if (!$scope.trigger || !$scope.repository) { return; }
|
||
|
||
$scope.currentView = 'config';
|
||
$('#setupTriggerModal').modal({});
|
||
|
||
if (!modalSetup) {
|
||
$('#setupTriggerModal').on('hidden.bs.modal', function () {
|
||
if (!$scope.trigger || $scope.trigger['is_active']) { return; }
|
||
|
||
$scope.nextStepCounter = -1;
|
||
$scope.$apply(function() {
|
||
$scope.cancelSetupTrigger();
|
||
});
|
||
});
|
||
|
||
modalSetup = true;
|
||
$scope.nextStepCounter = 0;
|
||
}
|
||
};
|
||
|
||
$scope.isNamespaceAdmin = function(namespace) {
|
||
return UserService.isNamespaceAdmin(namespace);
|
||
};
|
||
|
||
$scope.cancelSetupTrigger = function() {
|
||
$scope.canceled({'trigger': $scope.trigger});
|
||
};
|
||
|
||
$scope.hide = function() {
|
||
$('#setupTriggerModal').modal('hide');
|
||
};
|
||
|
||
$scope.checkAnalyze = function(isValid) {
|
||
$scope.currentView = 'analyzing';
|
||
$scope.pullInfo = {
|
||
'is_public': true
|
||
};
|
||
|
||
if (!isValid) {
|
||
$scope.currentView = 'analyzed';
|
||
return;
|
||
}
|
||
|
||
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.currentView = 'analyzed';
|
||
|
||
if (resp['status'] == 'analyzed') {
|
||
if (resp['robots'] && resp['robots'].length > 0) {
|
||
$scope.pullInfo['pull_entity'] = resp['robots'][0];
|
||
} else {
|
||
$scope.pullInfo['pull_entity'] = null;
|
||
}
|
||
|
||
$scope.pullInfo['is_public'] = false;
|
||
}
|
||
|
||
$scope.pullInfo['analysis'] = resp;
|
||
}, ApiService.errorDisplay('Cannot load Dockerfile information'));
|
||
};
|
||
|
||
$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.pullInfo['pull_entity']) {
|
||
data['pull_robot'] = $scope.pullInfo['pull_entity']['name'];
|
||
}
|
||
|
||
$scope.currentView = 'activating';
|
||
|
||
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',
|
||
|
||
'nextStepCounter': '=nextStepCounter',
|
||
'currentStepValid': '=currentStepValid',
|
||
|
||
'analyze': '&analyze'
|
||
},
|
||
controller: function($scope, $element, ApiService) {
|
||
$scope.analyzeCounter = 0;
|
||
$scope.setupReady = false;
|
||
$scope.refs = null;
|
||
$scope.branchNames = null;
|
||
$scope.tagNames = null;
|
||
|
||
$scope.state = {
|
||
'currentRepo': null,
|
||
'branchTagFilter': '',
|
||
'hasBranchTagFilter': false,
|
||
'isInvalidLocation': true,
|
||
'currentLocation': null
|
||
};
|
||
|
||
$scope.isMatching = function(kind, name, filter) {
|
||
try {
|
||
var patt = new RegExp(filter);
|
||
} catch (ex) {
|
||
return false;
|
||
}
|
||
|
||
var fullname = (kind + '/' + name);
|
||
var m = fullname.match(patt);
|
||
return m && m[0].length == fullname.length;
|
||
}
|
||
|
||
$scope.addRef = function(kind, name) {
|
||
if ($scope.isMatching(kind, name, $scope.state.branchTagFilter)) {
|
||
return;
|
||
}
|
||
|
||
var newFilter = kind + '/' + name;
|
||
var existing = $scope.state.branchTagFilter;
|
||
if (existing) {
|
||
$scope.state.branchTagFilter = '(' + existing + ')|(' + newFilter + ')';
|
||
} else {
|
||
$scope.state.branchTagFilter = newFilter;
|
||
}
|
||
}
|
||
|
||
$scope.stepsCompleted = function() {
|
||
$scope.analyze({'isValid': !$scope.state.isInvalidLocation});
|
||
};
|
||
|
||
$scope.loadRepositories = function(callback) {
|
||
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();
|
||
callback();
|
||
}, ApiService.errorDisplay('Cannot load repositories'));
|
||
};
|
||
|
||
$scope.loadBranchesAndTags = function(callback) {
|
||
var params = {
|
||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||
'trigger_uuid': $scope.trigger['id'],
|
||
'field_name': 'refs'
|
||
};
|
||
|
||
ApiService.listTriggerFieldValues($scope.trigger['config'], params).then(function(resp) {
|
||
$scope.refs = resp['values'];
|
||
$scope.branchNames = [];
|
||
$scope.tagNames = [];
|
||
|
||
for (var i = 0; i < $scope.refs.length; ++i) {
|
||
var ref = $scope.refs[i];
|
||
if (ref.kind == 'branch') {
|
||
$scope.branchNames.push(ref.name);
|
||
} else {
|
||
$scope.tagNames.push(ref.name);
|
||
}
|
||
}
|
||
|
||
callback();
|
||
}, ApiService.errorDisplay('Cannot load branch and tag names'));
|
||
};
|
||
|
||
$scope.loadLocations = function(callback) {
|
||
$scope.locations = null;
|
||
|
||
var params = {
|
||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||
'trigger_uuid': $scope.trigger.id
|
||
};
|
||
|
||
ApiService.listBuildTriggerSubdirs($scope.trigger['config'], params).then(function(resp) {
|
||
if (resp['status'] == 'error') {
|
||
callback(resp['message'] || 'Could not load Dockerfile locations');
|
||
return;
|
||
}
|
||
|
||
$scope.locations = resp['subdir'] || [];
|
||
|
||
// Select a default location (if any).
|
||
if ($scope.locations.length > 0) {
|
||
$scope.setLocation($scope.locations[0]);
|
||
} else {
|
||
$scope.state.currentLocation = null;
|
||
$scope.state.isInvalidLocation = resp['subdir'].indexOf('') < 0;
|
||
$scope.trigger.$ready = true;
|
||
}
|
||
|
||
callback();
|
||
}, ApiService.errorDisplay('Cannot load locations'));
|
||
}
|
||
|
||
$scope.handleLocationInput = function(location) {
|
||
$scope.state.isInvalidLocation = $scope.locations.indexOf(location) < 0;
|
||
$scope.trigger['config']['subdir'] = location || '';
|
||
$scope.trigger.$ready = true;
|
||
};
|
||
|
||
$scope.handleLocationSelected = function(datum) {
|
||
$scope.setLocation(datum['value']);
|
||
};
|
||
|
||
$scope.setLocation = function(location) {
|
||
$scope.state.currentLocation = location;
|
||
$scope.state.isInvalidLocation = false;
|
||
$scope.trigger['config']['subdir'] = location || '';
|
||
$scope.trigger.$ready = true;
|
||
};
|
||
|
||
$scope.selectRepo = function(repo, org) {
|
||
$scope.state.currentRepo = {
|
||
'repo': repo,
|
||
'avatar_url': org['info']['avatar_url'],
|
||
'toString': function() {
|
||
return this.repo;
|
||
}
|
||
};
|
||
};
|
||
|
||
$scope.selectRepoInternal = function(currentRepo) {
|
||
$scope.trigger.$ready = false;
|
||
|
||
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': ''
|
||
};
|
||
};
|
||
|
||
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;
|
||
};
|
||
|
||
$scope.$watch('state.currentRepo', function(repo) {
|
||
if (repo) {
|
||
$scope.selectRepoInternal(repo);
|
||
}
|
||
});
|
||
|
||
$scope.$watch('state.branchTagFilter', function(bf) {
|
||
if (!$scope.trigger) { return; }
|
||
|
||
if ($scope.state.hasBranchTagFilter) {
|
||
$scope.trigger['config']['branchtag_regex'] = bf;
|
||
} else {
|
||
delete $scope.trigger['config']['branchtag_regex'];
|
||
}
|
||
});
|
||
}
|
||
};
|
||
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 '<i class="fa fa-hdd-o"></i> <a href="' + registryHandlers[registry](pieces) + '" target="_blank">' + title + '</a>';
|
||
}
|
||
};
|
||
|
||
$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 'priming-cache':
|
||
return 'Priming cache for build';
|
||
|
||
case 'pushing':
|
||
return 'Pushing image built from Dockerfile';
|
||
|
||
case 'complete':
|
||
return 'Dockerfile build completed and pushed';
|
||
|
||
case 'error':
|
||
return 'Dockerfile build failed';
|
||
|
||
case 'internalerror':
|
||
return 'An internal system error occurred while building; the build will be retried in the next few minutes.';
|
||
}
|
||
};
|
||
}
|
||
};
|
||
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 'priming-cache':
|
||
return buildInfo.status.cache_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.getPattern = function(field) {
|
||
return new RegExp(field.regex);
|
||
};
|
||
|
||
$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('usageChart', function () {
|
||
var directiveDefinitionObject = {
|
||
priority: 0,
|
||
templateUrl: '/static/directives/usage-chart.html',
|
||
replace: false,
|
||
transclude: false,
|
||
restrict: 'C',
|
||
scope: {
|
||
'current': '=current',
|
||
'total': '=total',
|
||
'limit': '=limit',
|
||
'usageTitle': '@usageTitle'
|
||
},
|
||
controller: function($scope, $element) {
|
||
$scope.limit = "";
|
||
|
||
var chart = null;
|
||
|
||
var update = function() {
|
||
if ($scope.current == null || $scope.total == null) { return; }
|
||
if (!chart) {
|
||
chart = new UsageChart();
|
||
chart.draw('usage-chart-element');
|
||
}
|
||
|
||
var current = $scope.current || 0;
|
||
var total = $scope.total || 0;
|
||
if (current > total) {
|
||
$scope.limit = 'over';
|
||
} else if (current == total) {
|
||
$scope.limit = 'at';
|
||
} else if (current >= total * 0.7) {
|
||
$scope.limit = 'near';
|
||
} else {
|
||
$scope.limit = 'none';
|
||
}
|
||
|
||
chart.update($scope.current, $scope.total);
|
||
};
|
||
|
||
$scope.$watch('current', update);
|
||
$scope.$watch('total', update);
|
||
}
|
||
};
|
||
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.getAvatar = function(orgname) {
|
||
var organization = UserService.getOrganization(orgname);
|
||
return organization['avatar'] || '';
|
||
};
|
||
|
||
$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);
|
||
};
|
||
|
||
$scope.getActions = function(notification) {
|
||
return NotificationService.getActions(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) + '<br>';
|
||
if (ping == null) {
|
||
tip += '(Loading)';
|
||
} else if (ping < 0) {
|
||
tip += '<br><b>Note: Could not contact server</b>';
|
||
} 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('avatar', function () {
|
||
var directiveDefinitionObject = {
|
||
priority: 0,
|
||
templateUrl: '/static/directives/avatar.html',
|
||
replace: false,
|
||
transclude: true,
|
||
restrict: 'C',
|
||
scope: {
|
||
'hash': '=hash',
|
||
'email': '=email',
|
||
'name': '=name',
|
||
'size': '=size'
|
||
},
|
||
controller: function($scope, $element, AvatarService) {
|
||
$scope.AvatarService = AvatarService;
|
||
|
||
var refreshHash = function() {
|
||
if (!$scope.name && !$scope.email) { return; }
|
||
$scope._hash = AvatarService.computeHash($scope.email, $scope.name);
|
||
};
|
||
|
||
$scope.$watch('hash', function(hash) {
|
||
$scope._hash = hash;
|
||
});
|
||
|
||
$scope.$watch('name', refreshHash);
|
||
$scope.$watch('email', refreshHash);
|
||
}
|
||
};
|
||
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.id == currentTag.image_id) {
|
||
classes += 'tag-image ';
|
||
}
|
||
|
||
return classes;
|
||
};
|
||
|
||
var forAllTagImages = function(tag, callback, opt_cutoff) {
|
||
if (!tag) { return; }
|
||
|
||
if (!$scope.imageByDockerId) {
|
||
$scope.imageByDockerId = [];
|
||
for (var i = 0; i < $scope.images.length; ++i) {
|
||
var currentImage = $scope.images[i];
|
||
$scope.imageByDockerId[currentImage.id] = currentImage;
|
||
}
|
||
}
|
||
|
||
var tag_image = $scope.imageByDockerId[tag.image_id];
|
||
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.imageByDockerId[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.id] = 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 id in getIdsForTag(currentTag)) {
|
||
delete toDelete[id];
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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.id]) {
|
||
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.sort_index - a.sort_index;
|
||
});
|
||
|
||
$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.config( [
|
||
'$compileProvider',
|
||
function( $compileProvider )
|
||
{
|
||
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|tel|irc):/);
|
||
}
|
||
]);
|
||
|
||
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 <meta> 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;
|
||
};
|
||
}]);
|