This repository has been archived on 2020-03-24. You can view files and clone it, but cannot push or open issues or pull requests.
quay/static/js/app.js
2014-10-07 14:03:17 -04:00

6467 lines
196 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

var TEAM_PATTERN = '^[a-zA-Z][a-zA-Z0-9]+$';
var ROBOT_PATTERN = '^[a-zA-Z][a-zA-Z0-9]{3,29}$';
$.fn.clipboardCopy = function() {
if (zeroClipboardSupported) {
(new ZeroClipboard($(this)));
return true;
}
this.hide();
return false;
};
var zeroClipboardSupported = true;
ZeroClipboard.config({
'swfPath': 'static/lib/ZeroClipboard.swf'
});
ZeroClipboard.on("error", function(e) {
zeroClipboardSupported = false;
});
ZeroClipboard.on('aftercopy', function(e) {
var container = e.target.parentNode.parentNode.parentNode;
var message = $(container).find('.clipboard-copied-message')[0];
// Resets the animation.
var elem = message;
elem.style.display = 'none';
elem.classList.remove('animated');
// Show the notification.
setTimeout(function() {
elem.style.display = 'inline-block';
elem.classList.add('animated');
}, 10);
// Reset the notification.
setTimeout(function() {
elem.style.display = 'none';
}, 5000);
});
function getRestUrl(args) {
var url = '';
for (var i = 0; i < arguments.length; ++i) {
if (i > 0) {
url += '/';
}
url += encodeURI(arguments[i])
}
return url;
}
function clickElement(el){
// From: http://stackoverflow.com/questions/16802795/click-not-working-in-mocha-phantomjs-on-certain-elements
var ev = document.createEvent("MouseEvent");
ev.initMouseEvent(
"click",
true /* bubble */, true /* cancelable */,
window, null,
0, 0, 0, 0, /* coordinates */
false, false, false, false, /* modifier keys */
0 /*left*/, null);
el.dispatchEvent(ev);
}
function getFirstTextLine(commentString) {
if (!commentString) { return ''; }
var lines = commentString.split('\n');
var MARKDOWN_CHARS = {
'#': true,
'-': true,
'>': true,
'`': true
};
for (var i = 0; i < lines.length; ++i) {
// Skip code lines.
if (lines[i].indexOf(' ') == 0) {
continue;
}
// Skip empty lines.
if ($.trim(lines[i]).length == 0) {
continue;
}
// Skip control lines.
if (MARKDOWN_CHARS[$.trim(lines[i])[0]]) {
continue;
}
return getMarkedDown(lines[i]);
}
return '';
}
function createRobotAccount(ApiService, is_org, orgname, name, callback) {
ApiService.createRobot(is_org ? orgname : null, null, {'robot_shortname': name})
.then(callback, ApiService.errorDisplay('Cannot create robot account'));
}
function createOrganizationTeam(ApiService, orgname, teamname, callback) {
var data = {
'name': teamname,
'role': 'member'
};
var params = {
'orgname': orgname,
'teamname': teamname
};
ApiService.updateOrganizationTeam(data, params)
.then(callback, ApiService.errorDisplay('Cannot create team'));
}
function getMarkedDown(string) {
return Markdown.getSanitizingConverter().makeHtml(string || '');
}
quayDependencies = ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment',
'mgcrea.ngStrap', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml',
'ngAnimate'];
if (window.__config && window.__config.MIXPANEL_KEY) {
quayDependencies.push('angulartics');
quayDependencies.push('angulartics.mixpanel');
}
quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoadingBarProvider) {
cfpLoadingBarProvider.includeSpinner = false;
/**
* Specialized wrapper around array which provides a toggle() method for viewing the contents of the
* array in a manner that is asynchronously filled in over a short time period. This prevents long
* pauses in the UI for ngRepeat's when the array is significant in size.
*/
$provide.factory('AngularViewArray', ['$interval', function($interval) {
var ADDTIONAL_COUNT = 20;
function _ViewArray() {
this.isVisible = false;
this.visibleEntries = null;
this.hasEntries = false;
this.entries = [];
this.timerRef_ = null;
this.currentIndex_ = 0;
}
_ViewArray.prototype.length = function() {
return this.entries.length;
};
_ViewArray.prototype.get = function(index) {
return this.entries[index];
};
_ViewArray.prototype.push = function(elem) {
this.entries.push(elem);
this.hasEntries = true;
if (this.isVisible) {
this.setVisible(true);
}
};
_ViewArray.prototype.toggle = function() {
this.setVisible(!this.isVisible);
};
_ViewArray.prototype.setVisible = function(newState) {
this.isVisible = newState;
this.visibleEntries = [];
this.currentIndex_ = 0;
if (newState) {
this.showAdditionalEntries_();
this.startTimer_();
} else {
this.stopTimer_();
}
};
_ViewArray.prototype.showAdditionalEntries_ = function() {
var i = 0;
for (i = this.currentIndex_; i < (this.currentIndex_ + ADDTIONAL_COUNT) && i < this.entries.length; ++i) {
this.visibleEntries.push(this.entries[i]);
}
this.currentIndex_ = i;
if (this.currentIndex_ >= this.entries.length) {
this.stopTimer_();
}
};
_ViewArray.prototype.startTimer_ = function() {
var that = this;
this.timerRef_ = $interval(function() {
that.showAdditionalEntries_();
}, 10);
};
_ViewArray.prototype.stopTimer_ = function() {
if (this.timerRef_) {
$interval.cancel(this.timerRef_);
this.timerRef_ = null;
}
};
var service = {
'create': function() {
return new _ViewArray();
}
};
return service;
}]);
/**
* Specialized class for conducting an HTTP poll, while properly preventing multiple calls.
*/
$provide.factory('AngularPollChannel', ['ApiService', '$timeout', function(ApiService, $timeout) {
var _PollChannel = function(scope, requester, opt_sleeptime) {
this.scope_ = scope;
this.requester_ = requester;
this.sleeptime_ = opt_sleeptime || (60 * 1000 /* 60s */);
this.timer_ = null;
this.working = false;
this.polling = false;
var that = this;
scope.$on('$destroy', function() {
that.stop();
});
};
_PollChannel.prototype.stop = function() {
if (this.timer_) {
$timeout.cancel(this.timer_);
this.timer_ = null;
this.polling_ = false;
}
this.working = false;
};
_PollChannel.prototype.start = function() {
// Make sure we invoke call outside the normal digest cycle, since
// we'll call $scope.$apply ourselves.
var that = this;
setTimeout(function() { that.call_(); }, 0);
};
_PollChannel.prototype.call_ = function() {
if (this.working) { return; }
var that = this;
this.working = true;
this.scope_.$apply(function() {
that.requester_(function(status) {
if (status) {
that.working = false;
that.setupTimer_();
} else {
that.stop();
}
});
});
};
_PollChannel.prototype.setupTimer_ = function() {
if (this.timer_) { return; }
var that = this;
this.polling = true;
this.timer_ = $timeout(function() {
that.timer_ = null;
that.call_();
}, this.sleeptime_)
};
var service = {
'create': function(scope, requester, opt_sleeptime) {
return new _PollChannel(scope, requester, opt_sleeptime);
}
};
return service;
}]);
$provide.factory('DataFileService', [function() {
var dataFileService = {};
dataFileService.getName_ = function(filePath) {
var parts = filePath.split('/');
return parts[parts.length - 1];
};
dataFileService.tryAsZip_ = function(buf, success, failure) {
var zip = null;
var zipFiles = null;
try {
var zip = new JSZip(buf);
zipFiles = zip.files;
} catch (e) {
failure();
return;
}
var files = [];
for (var filePath in zipFiles) {
if (zipFiles.hasOwnProperty(filePath)) {
files.push({
'name': dataFileService.getName_(filePath),
'path': filePath,
'canRead': true,
'toBlob': (function(fp) {
return function() {
return new Blob([zip.file(fp).asArrayBuffer()]);
};
}(filePath))
});
}
}
success(files);
};
dataFileService.tryAsTarGz_ = function(buf, success, failure) {
var gunzip = new Zlib.Gunzip(buf);
var plain = null;
try {
plain = gunzip.decompress();
} catch (e) {
failure();
return;
}
dataFileService.tryAsTar_(plain, success, failure);
};
dataFileService.tryAsTar_ = function(buf, success, failure) {
var collapsePath = function(originalPath) {
// Tar files can contain entries of the form './', so we need to collapse
// those paths down.
var parts = originalPath.split('/');
for (var i = parts.length - 1; i >= 0; i--) {
var part = parts[i];
if (part == '.') {
parts.splice(i, 1);
}
}
return parts.join('/');
};
var handler = new Untar(buf);
handler.process(function(status, read, files, err) {
switch (status) {
case 'error':
failure(err);
break;
case 'done':
var processed = [];
for (var i = 0; i < files.length; ++i) {
var currentFile = files[i];
var path = collapsePath(currentFile.meta.filename);
if (path == '' || path == 'pax_global_header') { continue; }
processed.push({
'name': dataFileService.getName_(path),
'path': path,
'canRead': true,
'toBlob': (function(currentFile) {
return function() {
return new Blob([currentFile.buffer], {type: 'application/octet-binary'});
};
}(currentFile))
});
}
success(processed);
break;
}
});
};
dataFileService.blobToString = function(blob, callback) {
var reader = new FileReader();
reader.onload = function(event){
callback(reader.result);
};
reader.readAsText(blob);
};
dataFileService.arrayToString = function(buf, callback) {
var bb = new Blob([buf], {type: 'application/octet-binary'});
var f = new FileReader();
f.onload = function(e) {
callback(e.target.result);
};
f.onerror = function(e) {
callback(null);
};
f.onabort = function(e) {
callback(null);
};
f.readAsText(bb);
};
dataFileService.readDataArrayAsPossibleArchive = function(buf, success, failure) {
dataFileService.tryAsZip_(buf, success, function() {
dataFileService.tryAsTarGz_(buf, success, failure);
});
};
dataFileService.downloadDataFileAsArrayBuffer = function($scope, url, progress, error, loaded) {
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'arraybuffer';
request.onprogress = function(e) {
$scope.$apply(function() {
var percentLoaded;
if (e.lengthComputable) {
progress(e.loaded / e.total);
}
});
};
request.onerror = function() {
$scope.$apply(function() {
error();
});
};
request.onload = function() {
if (this.status == 200) {
$scope.$apply(function() {
var uint8array = new Uint8Array(request.response);
loaded(uint8array);
});
return;
}
};
request.send();
};
return dataFileService;
}]);
$provide.factory('UIService', [function() {
var uiService = {};
uiService.hidePopover = function(elem) {
var popover = $(elem).data('bs.popover');
if (popover) {
popover.hide();
}
};
uiService.showPopover = function(elem, content) {
var popover = $(elem).data('bs.popover');
if (!popover) {
$(elem).popover({'content': '-', 'placement': 'left'});
}
setTimeout(function() {
var popover = $(elem).data('bs.popover');
popover.options.content = content;
popover.show();
}, 500);
};
uiService.showFormError = function(elem, result) {
var message = result.data['message'] || result.data['error_description'] || '';
if (message) {
uiService.showPopover(elem, message);
} else {
uiService.hidePopover(elem);
}
};
return uiService;
}]);
$provide.factory('UtilService', ['$sanitize', function($sanitize) {
var utilService = {};
utilService.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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
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('TriggerService', ['UtilService', '$sanitize', function(UtilService, $sanitize) {
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'
}
]
}
}
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'
},
{
'id': 'build_queued',
'title': 'Dockerfile Build Queued',
'icon': 'fa-tasks'
},
{
'id': 'build_start',
'title': 'Dockerfile Build Started',
'icon': 'fa-circle-o-notch'
},
{
'id': 'build_success',
'title': 'Dockerfile Build Successfully Completed',
'icon': 'fa-check-circle-o'
},
{
'id': 'build_failure',
'title': 'Dockerfile Build Failed',
'icon': 'fa-times-circle-o'
}
];
var methods = [
{
'id': 'quay_notification',
'title': Config.REGISTRY_TITLE + ' Notification',
'icon': 'quay-icon',
'fields': [
{
'name': 'target',
'type': 'entity',
'title': 'Recipient'
}
]
},
{
'id': 'email',
'title': 'E-mail',
'icon': 'fa-envelope',
'fields': [
{
'name': 'email',
'type': 'email',
'title': 'E-mail address'
}
],
'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': 'subdomain',
'type': 'string',
'title': 'Slack Subdomain'
},
{
'name': 'token',
'type': 'string',
'title': 'Token',
'help_url': 'https://{subdomain}.slack.com/services/new/incoming-webhook'
}
]
}
];
var methodMap = {};
var eventMap = {};
for (var i = 0; i < methods.length; ++i) {
methodMap[methods[i].id] = methods[i];
}
for (var i = 0; i < events.length; ++i) {
eventMap[events[i].id] = events[i];
}
externalNotificationData.getSupportedEvents = function() {
return events;
};
externalNotificationData.getSupportedMethods = function() {
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} has been pushed';
}
},
'page': function(metadata) {
return '/repository/' + metadata.repository;
},
'dismissable': true
},
'build_queued': {
'level': 'info',
'message': 'A build has been queued for repository {repository}',
'page': function(metadata) {
return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
},
'dismissable': true
},
'build_start': {
'level': 'info',
'message': 'A build has been started for repository {repository}',
'page': function(metadata) {
return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
},
'dismissable': true
},
'build_success': {
'level': 'info',
'message': 'A build has succeeded for repository {repository}',
'page': function(metadata) {
return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
},
'dismissable': true
},
'build_failure': {
'level': 'error',
'message': 'A build has failed for repository {repository}',
'page': function(metadata) {
return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
},
'dismissable': true
}
};
notificationService.dismissNotification = function(notification) {
notification.dismissed = true;
var params = {
'uuid': notification.id
};
ApiService.updateUserNotification(notification, params, function() {
notificationService.update();
}, ApiService.errorDisplay('Could not update notification'));
var index = $.inArray(notification, notificationService.notifications);
if (index >= 0) {
notificationService.notifications.splice(index, 1);
}
};
notificationService.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('KeyService', ['$location', 'Config', function($location, Config) {
var keyService = {}
keyService['stripePublishableKey'] = Config['STRIPE_PUBLISHABLE_KEY'];
keyService['githubClientId'] = Config['GITHUB_CLIENT_ID'];
keyService['githubLoginClientId'] = Config['GITHUB_LOGIN_CLIENT_ID'];
keyService['githubRedirectUri'] = Config.getUrl('/oauth2/github/callback');
keyService['googleLoginClientId'] = Config['GOOGLE_LOGIN_CLIENT_ID'];
keyService['googleRedirectUri'] = Config.getUrl('/oauth2/google/callback');
keyService['googleLoginUrl'] = 'https://accounts.google.com/o/oauth2/auth?response_type=code&';
keyService['githubLoginUrl'] = 'https://github.com/login/oauth/authorize?';
keyService['googleLoginScope'] = 'openid email';
keyService['githubLoginScope'] = 'user:email';
keyService.getExternalLoginUrl = function(service, action) {
var state_clause = '';
if (Config.MIXPANEL_KEY && window.mixpanel) {
if (mixpanel.get_distinct_id !== undefined) {
state_clause = "&state=" + encodeURIComponent(mixpanel.get_distinct_id());
}
}
var client_id = keyService[service + 'LoginClientId'];
var scope = keyService[service + 'LoginScope'];
var redirect_uri = keyService[service + 'RedirectUri'];
if (action == 'attach') {
redirect_uri += '/attach';
}
var url = keyService[service + 'LoginUrl'] + 'client_id=' + client_id + '&scope=' + scope +
'&redirect_uri=' + redirect_uri + state_clause;
return url;
};
return keyService;
}]);
$provide.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService', 'Features', 'Config',
function(KeyService, UserService, CookieService, ApiService, Features, Config) {
var plans = null;
var planDict = {};
var planService = {};
var listeners = [];
var previousSubscribeFailure = false;
planService.getFreePlan = function() {
return 'free';
};
planService.registerListener = function(obj, callback) {
listeners.push({'obj': obj, 'callback': callback});
};
planService.unregisterListener = function(obj) {
for (var i = 0; i < listeners.length; ++i) {
if (listeners[i].obj == obj) {
listeners.splice(i, 1);
break;
}
}
};
planService.notePlan = function(planId) {
if (Features.BILLING) {
CookieService.putSession('quay.notedplan', planId);
}
};
planService.isOrgCompatible = function(plan) {
return plan['stripeId'] == planService.getFreePlan() || plan['bus_features'];
};
planService.getMatchingBusinessPlan = function(callback) {
planService.getPlans(function() {
planService.getSubscription(null, function(sub) {
var plan = planDict[sub.plan];
if (!plan) {
planService.getMinimumPlan(0, true, callback);
return;
}
var count = Math.max(sub.usedPrivateRepos, plan.privateRepos);
planService.getMinimumPlan(count, true, callback);
}, function() {
planService.getMinimumPlan(0, true, callback);
});
});
};
planService.handleNotedPlan = function() {
var planId = planService.getAndResetNotedPlan();
if (!planId || !Features.BILLING) { return false; }
UserService.load(function() {
if (UserService.currentUser().anonymous) {
return;
}
planService.getPlan(planId, function(plan) {
if (planService.isOrgCompatible(plan)) {
document.location = '/organizations/new/?plan=' + planId;
} else {
document.location = '/user?plan=' + planId;
}
});
});
return true;
};
planService.getAndResetNotedPlan = function() {
var planId = CookieService.get('quay.notedplan');
CookieService.clear('quay.notedplan');
return planId;
};
planService.handleCardError = function(resp) {
if (!planService.isCardError(resp)) { return; }
bootbox.dialog({
"message": resp.data.carderror,
"title": "Credit card issue",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
};
planService.isCardError = function(resp) {
return resp && resp.data && resp.data.carderror;
};
planService.verifyLoaded = function(callback) {
if (!Features.BILLING) { return; }
if (plans && plans.length) {
callback(plans);
return;
}
ApiService.listPlans().then(function(data) {
plans = data.plans || [];
for(var i = 0; i < plans.length; i++) {
planDict[plans[i].stripeId] = plans[i];
}
callback(plans);
}, function() { callback([]); });
};
planService.getPlans = function(callback, opt_includePersonal) {
planService.verifyLoaded(function() {
var filtered = [];
for (var i = 0; i < plans.length; ++i) {
var plan = plans[i];
if (plan['deprecated']) { continue; }
if (!opt_includePersonal && !planService.isOrgCompatible(plan)) { continue; }
filtered.push(plan);
}
callback(filtered);
});
};
planService.getPlan = function(planId, callback) {
planService.getPlanIncludingDeprecated(planId, function(plan) {
if (!plan['deprecated']) {
callback(plan);
}
});
};
planService.getPlanIncludingDeprecated = function(planId, callback) {
planService.verifyLoaded(function() {
if (planDict[planId]) {
callback(planDict[planId]);
}
});
};
planService.getMinimumPlan = function(privateCount, isBusiness, callback) {
planService.getPlans(function(plans) {
for (var i = 0; i < plans.length; i++) {
var plan = plans[i];
if (plan.privateRepos >= privateCount) {
callback(plan);
return;
}
}
callback(null);
}, /* include personal */!isBusiness);
};
planService.getSubscription = function(orgname, success, failure) {
if (!Features.BILLING) { return; }
ApiService.getSubscription(orgname).then(success, failure);
};
planService.setSubscription = function(orgname, planId, success, failure, opt_token) {
if (!Features.BILLING) { return; }
var subscriptionDetails = {
plan: planId
};
if (opt_token) {
subscriptionDetails['token'] = opt_token.id;
}
ApiService.updateSubscription(orgname, subscriptionDetails).then(function(resp) {
success(resp);
planService.getPlan(planId, function(plan) {
for (var i = 0; i < listeners.length; ++i) {
listeners[i]['callback'](plan);
}
});
}, failure);
};
planService.getCardInfo = function(orgname, callback) {
if (!Features.BILLING) { return; }
ApiService.getCard(orgname).then(function(resp) {
callback(resp.card);
}, function() {
callback({'is_valid': false});
});
};
planService.changePlan = function($scope, orgname, planId, callbacks, opt_async) {
if (!Features.BILLING) { return; }
if (callbacks['started']) {
callbacks['started']();
}
planService.getPlan(planId, function(plan) {
if (orgname && !planService.isOrgCompatible(plan)) { return; }
planService.getCardInfo(orgname, function(cardInfo) {
if (plan.price > 0 && (previousSubscribeFailure || !cardInfo.last4)) {
var title = cardInfo.last4 ? 'Subscribe' : 'Start Trial ({{amount}} plan)';
planService.showSubscribeDialog($scope, orgname, planId, callbacks, title, /* async */true);
return;
}
previousSubscribeFailure = false;
planService.setSubscription(orgname, planId, callbacks['success'], function(resp) {
previousSubscribeFailure = true;
planService.handleCardError(resp);
callbacks['failure'](resp);
});
});
});
};
planService.changeCreditCard = function($scope, orgname, callbacks) {
if (!Features.BILLING) { return; }
if (callbacks['opening']) {
callbacks['opening']();
}
var submitted = false;
var submitToken = function(token) {
if (submitted) { return; }
submitted = true;
$scope.$apply(function() {
if (callbacks['started']) {
callbacks['started']();
}
var cardInfo = {
'token': token.id
};
ApiService.setCard(orgname, cardInfo).then(callbacks['success'], function(resp) {
planService.handleCardError(resp);
callbacks['failure'](resp);
});
});
};
var email = planService.getEmail(orgname);
StripeCheckout.open({
key: KeyService.stripePublishableKey,
address: false,
email: email,
currency: 'usd',
name: 'Update credit card',
description: 'Enter your credit card number',
panelLabel: 'Update',
token: submitToken,
image: 'static/img/quay-icon-stripe.png',
opened: function() { $scope.$apply(function() { callbacks['opened']() }); },
closed: function() { $scope.$apply(function() { callbacks['closed']() }); }
});
};
planService.getEmail = function(orgname) {
var email = null;
if (UserService.currentUser()) {
email = UserService.currentUser().email;
if (orgname) {
org = UserService.getOrganization(orgname);
if (org) {
emaiil = org.email;
}
}
}
return email;
};
planService.showSubscribeDialog = function($scope, orgname, planId, callbacks, opt_title, opt_async) {
if (!Features.BILLING) { return; }
// If the async parameter is true and this is a browser that does not allow async popup of the
// Stripe dialog (such as Mobile Safari or IE), show a bootbox to show the dialog instead.
var isIE = navigator.appName.indexOf("Internet Explorer") != -1;
var isMobileSafari = navigator.userAgent.match(/(iPod|iPhone|iPad)/) && navigator.userAgent.match(/AppleWebKit/);
if (opt_async && (isIE || isMobileSafari)) {
bootbox.dialog({
"message": "Please click 'Subscribe' to continue",
"buttons": {
"subscribe": {
"label": "Subscribe",
"className": "btn-primary",
"callback": function() {
planService.showSubscribeDialog($scope, orgname, planId, callbacks, opt_title, false);
}
},
"close": {
"label": "Cancel",
"className": "btn-default"
}
}
});
return;
}
if (callbacks['opening']) {
callbacks['opening']();
}
var submitted = false;
var submitToken = function(token) {
if (submitted) { return; }
submitted = true;
if (Config.MIXPANEL_KEY) {
mixpanel.track('plan_subscribe');
}
$scope.$apply(function() {
if (callbacks['started']) {
callbacks['started']();
}
planService.setSubscription(orgname, planId, callbacks['success'], callbacks['failure'], token);
});
};
planService.getPlan(planId, function(planDetails) {
var email = planService.getEmail(orgname);
StripeCheckout.open({
key: KeyService.stripePublishableKey,
address: false,
email: email,
amount: planDetails.price,
currency: 'usd',
name: 'Quay.io ' + planDetails.title + ' Subscription',
description: 'Up to ' + planDetails.privateRepos + ' private repositories',
panelLabel: opt_title || 'Subscribe',
token: submitToken,
image: 'static/img/quay-icon-stripe.png',
opened: function() { $scope.$apply(function() { callbacks['opened']() }); },
closed: function() { $scope.$apply(function() { callbacks['closed']() }); }
});
});
};
return planService;
}]);
}).
directive('match', function($parse) {
return {
require: 'ngModel',
link: function(scope, elem, attrs, ctrl) {
scope.$watch(function() {
return $parse(attrs.match)(scope) === ctrl.$modelValue;
}, function(currentValue) {
ctrl.$setValidity('mismatch', currentValue);
});
}
};
}).
directive('onresize', function ($window, $parse) {
return function (scope, element, attr) {
var fn = $parse(attr.onresize);
var notifyResized = function() {
scope.$apply(function () {
fn(scope);
});
};
angular.element($window).on('resize', null, notifyResized);
scope.$on('$destroy', function() {
angular.element($window).off('resize', null, notifyResized);
});
};
}).
config(['$routeProvider', '$locationProvider',
function($routeProvider, $locationProvider) {
var title = window.__config['REGISTRY_TITLE'] || 'Quay.io';
$locationProvider.html5Mode(true);
// WARNING WARNING WARNING
// If you add a route here, you must add a corresponding route in thr endpoints/web.py
// index rule to make sure that deep links directly deep into the app continue to work.
// WARNING WARNING WARNING
$routeProvider.
when('/repository/:namespace/:name', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl,
fixFooter: false, reloadOnSearch: false}).
when('/repository/:namespace/:name/tag/:tag', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl,
fixFooter: false}).
when('/repository/:namespace/:name/image/:image', {templateUrl: '/static/partials/image-view.html', controller: ImageViewCtrl, reloadOnSearch: false}).
when('/repository/:namespace/:name/admin', {templateUrl: '/static/partials/repo-admin.html', controller:RepoAdminCtrl, reloadOnSearch: false}).
when('/repository/:namespace/:name/build', {templateUrl: '/static/partials/repo-build.html', controller:RepoBuildCtrl, reloadOnSearch: false}).
when('/repository/:namespace/:name/build/:buildid/buildpack', {templateUrl: '/static/partials/build-package.html', controller:BuildPackageCtrl, reloadOnSearch: false}).
when('/repository/', {title: 'Repositories', description: 'Public and private docker repositories list',
templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl, reloadOnSearch: false}).
when('/user/', {title: 'Account Settings', description:'Account settings for ' + title, templateUrl: '/static/partials/user-admin.html',
reloadOnSearch: false, controller: UserAdminCtrl}).
when('/superuser/', {title: 'Superuser Admin Panel', description:'Admin panel for ' + title, templateUrl: '/static/partials/super-user.html',
reloadOnSearch: false, controller: SuperUserAdminCtrl}).
when('/guide/', {title: 'Guide', description:'Guide to using private docker repositories on ' + title,
templateUrl: '/static/partials/guide.html',
controller: GuideCtrl}).
when('/tutorial/', {title: 'Tutorial', description:'Interactive tutorial for using ' + title, templateUrl: '/static/partials/tutorial.html',
controller: TutorialCtrl}).
when('/contact/', {title: 'Contact Us', description:'Different ways for you to get a hold of us when you need us most.', templateUrl: '/static/partials/contact.html',
controller: ContactCtrl}).
when('/about/', {title: 'About Us', description:'Information about the Quay.io team and the company.', templateUrl: '/static/partials/about.html'}).
when('/plans/', {title: 'Plans and Pricing', description: 'Plans and pricing for private docker repositories on Quay.io',
templateUrl: '/static/partials/plans.html', controller: PlansCtrl}).
when('/security/', {title: 'Security', description: 'Security features used when transmitting and storing data',
templateUrl: '/static/partials/security.html'}).
when('/signin/', {title: 'Sign In', description: 'Sign into ' + title, templateUrl: '/static/partials/signin.html', 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',
'showGravatar': '@showGravatar',
'gravatarSize': '@gravatarSize'
},
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');
scope.$apply(function() {
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(result) {
$scope.invalidRecovery = true;
$scope.errorMessage = result.data;
$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.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': '&regenerate'
},
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('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}',
'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',
'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) {
window.console.log('creating chart');
$scope.chart = new LogUsageChart(logKinds);
$($scope.chart).bind('filteringChanged', function(e) {
$scope.$apply(function() { $scope.kindsAllowed = e.allowed; });
});
}
$scope.chart.draw('bar-chart', resp.logs, $scope.logStartDate, $scope.logEndDate);
$scope.kindsAllowed = null;
$scope.logs = resp.logs;
$scope.loading = false;
});
};
$scope.toggleChart = function() {
$scope.chartVisible = !$scope.chartVisible;
};
$scope.isVisible = function(allowed, kind) {
return allowed == null || allowed.hasOwnProperty(kind);
};
$scope.getColor = function(kind) {
return $scope.chart.getColor(kind);
};
$scope.getDescription = function(log) {
log.metadata['_ip'] = log.ip ? log.ip : null;
return StringBuilderService.buildString(logDescriptions[log.kind] || log.kind, log.metadata);
};
$scope.$watch('organization', update);
$scope.$watch('user', update);
$scope.$watch('repository', update);
$scope.$watch('makevisible', update);
$scope.$watch('performer', update);
$scope.$watch('logStartDate', update);
$scope.$watch('logEndDate', update);
}
};
return directiveDefinitionObject;
});
quayApp.directive('applicationManager', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/application-manager.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'organization': '=organization',
'makevisible': '=makevisible'
},
controller: function($scope, $element, ApiService) {
$scope.loading = false;
$scope.applications = [];
$scope.createApplication = function(appName) {
if (!appName) { return; }
var params = {
'orgname': $scope.organization.name
};
var data = {
'name': appName
};
ApiService.createOrganizationApplication(data, params).then(function(resp) {
$scope.applications.push(resp);
}, ApiService.errorDisplay('Cannot create application'));
};
var update = function() {
if (!$scope.organization || !$scope.makevisible) { return; }
if ($scope.loading) { return; }
$scope.loading = true;
var params = {
'orgname': $scope.organization.name
};
ApiService.getOrganizationApplications(null, params).then(function(resp) {
$scope.loading = false;
$scope.applications = resp['applications'] || [];
});
};
$scope.$watch('organization', update);
$scope.$watch('makevisible', update);
}
};
return directiveDefinitionObject;
});
quayApp.directive('robotsManager', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/robots-manager.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'organization': '=organization',
'user': '=user'
},
controller: function($scope, $element, ApiService, $routeParams) {
$scope.ROBOT_PATTERN = ROBOT_PATTERN;
$scope.robots = null;
$scope.loading = false;
$scope.shownRobot = null;
$scope.showRobotCounter = 0;
$scope.regenerateToken = function(username) {
if (!username) { return; }
var shortName = $scope.getShortenedName(username);
ApiService.regenerateRobotToken($scope.organization, null, {'robot_shortname': shortName}).then(function(updated) {
var index = $scope.findRobotIndexByName(username);
if (index >= 0) {
$scope.robots.splice(index, 1);
$scope.robots.push(updated);
}
$scope.shownRobot = updated;
}, ApiService.errorDisplay('Cannot regenerate robot account token'));
};
$scope.showRobot = function(info) {
$scope.shownRobot = info;
$scope.showRobotCounter++;
};
$scope.findRobotIndexByName = function(name) {
for (var i = 0; i < $scope.robots.length; ++i) {
if ($scope.robots[i].name == name) {
return i;
}
}
return -1;
};
$scope.getShortenedName = function(name) {
var plus = name.indexOf('+');
return name.substr(plus + 1);
};
$scope.getPrefix = function(name) {
var plus = name.indexOf('+');
return name.substr(0, plus);
};
$scope.createRobot = function(name) {
if (!name) { return; }
createRobotAccount(ApiService, !!$scope.organization, $scope.organization ? $scope.organization.name : '', name,
function(created) {
$scope.robots.push(created);
});
};
$scope.deleteRobot = function(info) {
var shortName = $scope.getShortenedName(info.name);
ApiService.deleteRobot($scope.organization, null, {'robot_shortname': shortName}).then(function(resp) {
var index = $scope.findRobotIndexByName(info.name);
if (index >= 0) {
$scope.robots.splice(index, 1);
}
}, ApiService.errorDisplay('Cannot delete robot account'));
};
var update = function() {
if (!$scope.user && !$scope.organization) { return; }
if ($scope.loading) { return; }
$scope.loading = true;
ApiService.getRobots($scope.organization).then(function(resp) {
$scope.robots = resp.robots;
$scope.loading = false;
if ($routeParams.showRobot) {
var index = $scope.findRobotIndexByName($routeParams.showRobot);
if (index >= 0) {
$scope.showRobot($scope.robots[index]);
}
}
});
};
$scope.$watch('organization', update);
$scope.$watch('user', update);
}
};
return directiveDefinitionObject;
});
quayApp.directive('prototypeManager', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/prototype-manager.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'organization': '=organization'
},
controller: function($scope, $element, ApiService) {
$scope.loading = false;
$scope.activatingForNew = null;
$scope.delegateForNew = null;
$scope.clearCounter = 0;
$scope.newForWholeOrg = true;
$scope.roles = [
{ 'id': 'read', 'title': 'Read', 'kind': 'success' },
{ 'id': 'write', 'title': 'Write', 'kind': 'success' },
{ 'id': 'admin', 'title': 'Admin', 'kind': 'primary' }
];
$scope.setRole = function(role, prototype) {
var params = {
'orgname': $scope.organization.name,
'prototypeid': prototype.id
};
var data = {
'id': prototype.id,
'role': role
};
ApiService.updateOrganizationPrototypePermission(data, params).then(function(resp) {
prototype.role = role;
}, ApiService.errorDisplay('Cannot modify permission'));
};
$scope.comparePrototypes = function(p) {
return p.activating_user ? p.activating_user.name : ' ';
};
$scope.setRoleForNew = function(role) {
$scope.newRole = role;
};
$scope.setNewForWholeOrg = function(value) {
$scope.newForWholeOrg = value;
};
$scope.showAddDialog = function() {
$scope.activatingForNew = null;
$scope.delegateForNew = null;
$scope.newRole = 'read';
$scope.clearCounter++;
$scope.newForWholeOrg = true;
$('#addPermissionDialogModal').modal({});
};
$scope.createPrototype = function() {
$scope.loading = true;
var params = {
'orgname': $scope.organization.name
};
var data = {
'delegate': $scope.delegateForNew,
'role': $scope.newRole
};
if (!$scope.newForWholeOrg) {
data['activating_user'] = $scope.activatingForNew;
}
var errorHandler = ApiService.errorDisplay('Cannot create permission',
function(resp) {
$('#addPermissionDialogModal').modal('hide');
});
ApiService.createOrganizationPrototypePermission(data, params).then(function(resp) {
$scope.prototypes.push(resp);
$scope.loading = false;
$('#addPermissionDialogModal').modal('hide');
}, errorHandler);
};
$scope.deletePrototype = function(prototype) {
$scope.loading = true;
var params = {
'orgname': $scope.organization.name,
'prototypeid': prototype.id
};
ApiService.deleteOrganizationPrototypePermission(null, params).then(function(resp) {
$scope.prototypes.splice($scope.prototypes.indexOf(prototype), 1);
$scope.loading = false;
}, ApiService.errorDisplay('Cannot delete permission'));
};
var update = function() {
if (!$scope.organization) { return; }
if ($scope.loading) { return; }
var params = {'orgname': $scope.organization.name};
$scope.loading = true;
ApiService.getOrganizationPrototypePermissions(null, params).then(function(resp) {
$scope.prototypes = resp.prototypes;
$scope.loading = false;
});
};
$scope.$watch('organization', update);
}
};
return directiveDefinitionObject;
});
quayApp.directive('deleteUi', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/delete-ui.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'deleteTitle': '=deleteTitle',
'buttonTitle': '=buttonTitle',
'performDelete': '&performDelete'
},
controller: function($scope, $element) {
$scope.buttonTitleInternal = $scope.buttonTitle || 'Delete';
$element.children().attr('tabindex', 0);
$scope.focus = function() {
$element[0].firstChild.focus();
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('popupInputButton', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/popup-input-button.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'placeholder': '=placeholder',
'pattern': '=pattern',
'submitted': '&submitted'
},
controller: function($scope, $element) {
$scope.popupShown = function() {
setTimeout(function() {
var box = $('#input-box');
box[0].value = '';
box.focus();
}, 40);
};
$scope.getRegexp = function(pattern) {
if (!pattern) {
pattern = '.*';
}
return new RegExp(pattern);
};
$scope.inputSubmit = function() {
var box = $('#input-box');
if (box.hasClass('ng-invalid')) { return; }
var entered = box[0].value;
if (!entered) {
return;
}
if ($scope.submitted) {
$scope.submitted({'value': entered});
}
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('resourceView', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/resource-view.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'resource': '=resource',
'errorMessage': '=errorMessage'
},
controller: function($scope, $element) {
}
};
return directiveDefinitionObject;
});
quayApp.directive('quaySpinner', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/spinner.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {},
controller: function($scope, $element) {
}
};
return directiveDefinitionObject;
});
quayApp.directive('registryName', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/registry-name.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {},
controller: function($scope, $element, Config) {
$scope.name = Config.REGISTRY_TITLE;
}
};
return directiveDefinitionObject;
});
quayApp.directive('organizationHeader', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/organization-header.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'organization': '=organization',
'teamName': '=teamName',
'clickable': '=clickable'
},
controller: function($scope, $element) {
}
};
return directiveDefinitionObject;
});
quayApp.directive('markdownInput', function () {
var counter = 0;
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/markdown-input.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'content': '=content',
'canWrite': '=canWrite',
'contentChanged': '=contentChanged',
'fieldTitle': '=fieldTitle'
},
controller: function($scope, $element) {
var elm = $element[0];
$scope.id = (counter++);
$scope.editContent = function() {
if (!$scope.canWrite) { return; }
if (!$scope.markdownDescriptionEditor) {
var converter = Markdown.getSanitizingConverter();
var editor = new Markdown.Editor(converter, '-description-' + $scope.id);
editor.run();
$scope.markdownDescriptionEditor = editor;
}
$('#wmd-input-description-' + $scope.id)[0].value = $scope.content;
$(elm).find('.modal').modal({});
};
$scope.saveContent = function() {
$scope.content = $('#wmd-input-description-' + $scope.id)[0].value;
$(elm).find('.modal').modal('hide');
if ($scope.contentChanged) {
$scope.contentChanged($scope.content);
}
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('repoSearch', function () {
var number = 0;
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/repo-search.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
},
controller: function($scope, $element, $location, UserService, Restangular) {
var searchToken = 0;
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
++searchToken;
}, true);
var repoHound = new Bloodhound({
name: 'repositories',
remote: {
url: '/api/v1/find/repository?query=%QUERY',
replace: function (url, uriEncodedQuery) {
url = url.replace('%QUERY', uriEncodedQuery);
url += '&cb=' + searchToken;
return url;
},
filter: function(data) {
var datums = [];
for (var i = 0; i < data.repositories.length; ++i) {
var repo = data.repositories[i];
datums.push({
'value': repo.name,
'tokens': [repo.name, repo.namespace],
'repo': repo
});
}
return datums;
}
},
datumTokenizer: function(d) {
return Bloodhound.tokenizers.whitespace(d.val);
},
queryTokenizer: Bloodhound.tokenizers.whitespace
});
repoHound.initialize();
var element = $($element[0].childNodes[0]);
element.typeahead({ 'highlight': true }, {
source: repoHound.ttAdapter(),
templates: {
'suggestion': function (datum) {
template = '<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) {
$scope.notificationService = NotificationService;
// Monitor any user changes and place the current user into the scope.
UserService.updateUserIn($scope);
$scope.signout = function() {
ApiService.logout().then(function() {
UserService.load();
$location.path('/');
});
};
$scope.appLinkTarget = function() {
if ($("div[ng-view]").length === 0) {
return "_self";
}
return "";
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('entitySearch', function () {
var number = 0;
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/entity-search.html',
replace: false,
transclude: false,
restrict: 'C',
require: '?ngModel',
link: function(scope, element, attr, ctrl) {
scope.ngModel = ctrl;
},
scope: {
'namespace': '=namespace',
'placeholder': '=placeholder',
// Default: ['user', 'team', 'robot']
'allowedEntities': '=allowedEntities',
'currentEntity': '=currentEntity',
'entitySelected': '&entitySelected',
'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"><img src="//www.gravatar.com/avatar/' +
datum.entity.gravatar + '?s=16&amp;d=identicon"></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 });
}
if (sub.usedPrivateRepos > $scope.subscribedPlan.privateRepos) {
$scope.limit = 'over';
} else if (sub.usedPrivateRepos == $scope.subscribedPlan.privateRepos) {
$scope.limit = 'at';
} else if (sub.usedPrivateRepos >= $scope.subscribedPlan.privateRepos * 0.7) {
$scope.limit = 'near';
} else {
$scope.limit = 'none';
}
if (!$scope.chart) {
$scope.chart = new UsageChart();
$scope.chart.draw('repository-usage-chart');
}
$scope.chart.update(sub.usedPrivateRepos || 0, $scope.subscribedPlan.privateRepos || 0);
$scope.planChanging = false;
$scope.planLoading = false;
});
};
var update = function() {
$scope.planLoading = true;
if (!$scope.plans) { return; }
PlanService.getSubscription($scope.organization, subscribedToPlan, function() {
$scope.isExistingCustomer = false;
subscribedToPlan({ 'plan': PlanService.getFreePlan() });
});
};
var loadPlans = function() {
if ($scope.plans || $scope.loadingPlans) { return; }
if (!$scope.user && !$scope.organization) { return; }
$scope.loadingPlans = true;
PlanService.verifyLoaded(function(plans) {
$scope.plans = plans;
update();
if ($scope.readyForPlan) {
var planRequested = $scope.readyForPlan();
if (planRequested && planRequested != PlanService.getFreePlan()) {
$scope.changeSubscription(planRequested, /* async */true);
}
}
});
};
// Start the initial download.
$scope.planLoading = true;
loadPlans();
$scope.$watch('organization', loadPlans);
$scope.$watch('user', loadPlans);
}
};
return directiveDefinitionObject;
});
quayApp.directive('namespaceSelector', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/namespace-selector.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'user': '=user',
'namespace': '=namespace',
'requireCreate': '=requireCreate'
},
controller: function($scope, $element, $routeParams, $location, CookieService) {
$scope.namespaces = {};
$scope.initialize = function(user) {
var preferredNamespace = user.username;
var namespaces = {};
namespaces[user.username] = user;
if (user.organizations) {
for (var i = 0; i < user.organizations.length; ++i) {
namespaces[user.organizations[i].name] = user.organizations[i];
if (user.organizations[i].preferred_namespace) {
preferredNamespace = user.organizations[i].name;
}
}
}
var initialNamespace = $routeParams['namespace'] || CookieService.get('quay.namespace') ||
preferredNamespace || $scope.user.username;
$scope.namespaces = namespaces;
$scope.setNamespace($scope.namespaces[initialNamespace]);
};
$scope.setNamespace = function(namespaceObj) {
if (!namespaceObj) {
namespaceObj = $scope.namespaces[$scope.user.username];
}
if ($scope.requireCreate && !namespaceObj.can_create_repo) {
namespaceObj = $scope.namespaces[$scope.user.username];
}
var newNamespace = namespaceObj.name || namespaceObj.username;
$scope.namespaceObj = namespaceObj;
$scope.namespace = newNamespace;
if (newNamespace) {
CookieService.putPermanent('quay.namespace', newNamespace);
if ($routeParams['namespace'] && $routeParams['namespace'] != newNamespace) {
$location.search({'namespace': newNamespace});
}
}
};
$scope.$watch('namespace', function(namespace) {
if ($scope.namespaceObj && namespace && namespace != $scope.namespaceObj.username) {
$scope.setNamespace($scope.namespaces[namespace]);
}
});
$scope.$watch('user', function(user) {
$scope.user = user;
$scope.initialize(user);
});
}
};
return directiveDefinitionObject;
});
quayApp.directive('buildLogPhase', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/build-log-phase.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'phase': '=phase'
},
controller: function($scope, $element) {
}
};
return directiveDefinitionObject;
});
quayApp.directive('buildLogError', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/build-log-error.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'error': '=error',
'entries': '=entries'
},
controller: function($scope, $element, Config) {
$scope.getLocalPullInfo = function() {
if ($scope.entries.__localpull !== undefined) {
return $scope.entries.__localpull;
}
var localInfo = {
'isLocal': false
};
// Find the 'pulling' phase entry, and then extra any metadata found under
// it.
for (var i = 0; i < $scope.entries.length; ++i) {
var entry = $scope.entries[i];
if (entry.type == 'phase' && entry.message == 'pulling') {
for (var j = 0; j < entry.logs.length(); ++j) {
var log = entry.logs.get(j);
if (log.data && log.data.phasestep == 'login') {
localInfo['login'] = log.data;
}
if (log.data && log.data.phasestep == 'pull') {
var repo_url = log.data['repo_url'];
var repo_and_tag = repo_url.substring(Config.SERVER_HOSTNAME.length + 1);
var tagIndex = repo_and_tag.lastIndexOf(':');
var repo = repo_and_tag.substring(0, tagIndex);
localInfo['repo_url'] = repo_url;
localInfo['repo'] = repo;
localInfo['isLocal'] = repo_url.indexOf(Config.SERVER_HOSTNAME + '/') == 0;
}
}
break;
}
}
return $scope.entries.__localpull = localInfo;
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('triggerDescription', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/trigger-description.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'trigger': '=trigger',
'short': '=short'
},
controller: function($scope, $element) {
}
};
return directiveDefinitionObject;
});
quayApp.directive('dropdownSelect', function ($compile) {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/dropdown-select.html',
replace: true,
transclude: true,
restrict: 'C',
scope: {
'selectedItem': '=selectedItem',
'placeholder': '=placeholder',
'lookaheadItems': '=lookaheadItems',
'allowCustomInput': '@allowCustomInput',
'handleItemSelected': '&handleItemSelected',
'handleInput': '&handleInput',
'clearValue': '=clearValue'
},
controller: function($scope, $element, $rootScope) {
if (!$rootScope.__dropdownSelectCounter) {
$rootScope.__dropdownSelectCounter = 1;
}
$scope.placeholder = $scope.placeholder || '';
$scope.internalItem = null;
// Setup lookahead.
var input = $($element).find('.lookahead-input');
$scope.$watch('clearValue', function(cv) {
if (cv) {
$scope.selectedItem = null;
$(input).val('');
}
});
$scope.$watch('selectedItem', function(item) {
if ($scope.selectedItem == $scope.internalItem) {
// The item has already been set due to an internal action.
return;
}
if ($scope.selectedItem != null) {
$(input).val(item.toString());
} else {
$(input).val('');
}
});
$scope.$watch('lookaheadItems', function(items) {
$(input).off();
if (!items) {
return;
}
var formattedItems = [];
for (var i = 0; i < items.length; ++i) {
var formattedItem = items[i];
if (typeof formattedItem == 'string') {
formattedItem = {
'value': formattedItem
};
}
formattedItems.push(formattedItem);
}
var dropdownHound = new Bloodhound({
name: 'dropdown-items-' + $rootScope.__dropdownSelectCounter,
local: formattedItems,
datumTokenizer: function(d) {
return Bloodhound.tokenizers.whitespace(d.val || d.value || '');
},
queryTokenizer: Bloodhound.tokenizers.whitespace
});
dropdownHound.initialize();
$(input).typeahead({}, {
source: dropdownHound.ttAdapter(),
templates: {
'suggestion': function (datum) {
template = datum['template'] ? datum['template'](datum) : datum['value'];
return template;
}
}
});
$(input).on('input', function(e) {
$scope.$apply(function() {
$scope.internalItem = null;
$scope.selectedItem = null;
if ($scope.handleInput) {
$scope.handleInput({'input': $(input).val()});
}
});
});
$(input).on('typeahead:selected', function(e, datum) {
$scope.$apply(function() {
$scope.internalItem = datum['item'] || datum['value'];
$scope.selectedItem = datum['item'] || datum['value'];
if ($scope.handleItemSelected) {
$scope.handleItemSelected({'datum': datum});
}
});
});
$rootScope.__dropdownSelectCounter++;
});
},
link: function(scope, element, attrs) {
var transcludedBlock = element.find('div.transcluded');
var transcludedElements = transcludedBlock.children();
var iconContainer = element.find('div.dropdown-select-icon-transclude');
var menuContainer = element.find('div.dropdown-select-menu-transclude');
angular.forEach(transcludedElements, function(elem) {
if (angular.element(elem).hasClass('dropdown-select-icon')) {
iconContainer.append(elem);
} else if (angular.element(elem).hasClass('dropdown-select-menu')) {
menuContainer.replaceWith(elem);
}
});
transcludedBlock.remove();
}
};
return directiveDefinitionObject;
});
quayApp.directive('dropdownSelectIcon', function () {
var directiveDefinitionObject = {
priority: 1,
require: '^dropdownSelect',
templateUrl: '/static/directives/dropdown-select-icon.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
},
controller: function($scope, $element) {
}
};
return directiveDefinitionObject;
});
quayApp.directive('dropdownSelectMenu', function () {
var directiveDefinitionObject = {
priority: 1,
require: '^dropdownSelect',
templateUrl: '/static/directives/dropdown-select-menu.html',
replace: true,
transclude: true,
restrict: 'C',
scope: {
},
controller: function($scope, $element) {
}
};
return directiveDefinitionObject;
});
quayApp.directive('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.show = function() {
if (!$scope.trigger || !$scope.repository) { return; }
$scope.activating = false;
$scope.pullEntity = null;
$scope.publicPull = true;
$scope.showPullRequirements = false;
$('#setupTriggerModal').modal({});
if (!modalSetup) {
$('#setupTriggerModal').on('hidden.bs.modal', function () {
if (!$scope.trigger || $scope.trigger['is_active']) { return; }
$scope.$apply(function() {
$scope.cancelSetupTrigger();
});
});
modalSetup = true;
}
};
$scope.isNamespaceAdmin = function(namespace) {
return UserService.isNamespaceAdmin(namespace);
};
$scope.cancelSetupTrigger = function() {
$scope.canceled({'trigger': $scope.trigger});
};
$scope.hide = function() {
$scope.activating = false;
$('#setupTriggerModal').modal('hide');
};
$scope.setPublicPull = function(value) {
$scope.publicPull = value;
};
$scope.checkAnalyze = function(isValid) {
if (!isValid) {
$scope.publicPull = true;
$scope.pullEntity = null;
$scope.showPullRequirements = false;
$scope.checkingPullRequirements = false;
return;
}
$scope.checkingPullRequirements = true;
$scope.showPullRequirements = true;
$scope.pullRequirements = null;
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'trigger_uuid': $scope.trigger.id
};
var data = {
'config': $scope.trigger.config
};
ApiService.analyzeBuildTrigger(data, params).then(function(resp) {
$scope.pullRequirements = resp;
if (resp['status'] == 'publicbase') {
$scope.publicPull = true;
$scope.pullEntity = null;
} else if (resp['namespace']) {
$scope.publicPull = false;
if (resp['robots'] && resp['robots'].length > 0) {
$scope.pullEntity = resp['robots'][0];
} else {
$scope.pullEntity = null;
}
}
$scope.checkingPullRequirements = false;
}, function(resp) {
$scope.pullRequirements = resp;
$scope.checkingPullRequirements = false;
});
};
$scope.activate = function() {
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'trigger_uuid': $scope.trigger.id
};
var data = {
'config': $scope.trigger['config']
};
if ($scope.pullEntity) {
data['pull_robot'] = $scope.pullEntity['name'];
}
$scope.activating = true;
var errorHandler = ApiService.errorDisplay('Cannot activate build trigger', function(resp) {
$scope.hide();
$scope.canceled({'trigger': $scope.trigger});
});
ApiService.activateBuildTrigger(data, params).then(function(resp) {
$scope.hide();
$scope.trigger['is_active'] = true;
$scope.trigger['pull_robot'] = resp['pull_robot'];
$scope.activated({'trigger': $scope.trigger});
}, errorHandler);
};
var check = function() {
if ($scope.counter && $scope.trigger && $scope.repository) {
$scope.show();
}
};
$scope.$watch('trigger', check);
$scope.$watch('counter', check);
$scope.$watch('repository', check);
}
};
return directiveDefinitionObject;
});
quayApp.directive('triggerSetupGithub', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/trigger-setup-github.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'repository': '=repository',
'trigger': '=trigger',
'analyze': '&analyze'
},
controller: function($scope, $element, ApiService) {
$scope.analyzeCounter = 0;
$scope.setupReady = false;
$scope.loading = true;
$scope.handleLocationInput = function(location) {
$scope.trigger['config']['subdir'] = location || '';
$scope.isInvalidLocation = $scope.locations.indexOf(location) < 0;
$scope.analyze({'isValid': !$scope.isInvalidLocation});
};
$scope.handleLocationSelected = function(datum) {
$scope.setLocation(datum['value']);
};
$scope.setLocation = function(location) {
$scope.currentLocation = location;
$scope.trigger['config']['subdir'] = location || '';
$scope.isInvalidLocation = false;
$scope.analyze({'isValid': true});
};
$scope.selectRepo = function(repo, org) {
$scope.currentRepo = {
'repo': repo,
'avatar_url': org['info']['avatar_url'],
'toString': function() {
return this.repo;
}
};
};
$scope.selectRepoInternal = function(currentRepo) {
if (!currentRepo) {
$scope.trigger.$ready = false;
return;
}
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'trigger_uuid': $scope.trigger['id']
};
var repo = currentRepo['repo'];
$scope.trigger['config'] = {
'build_source': repo,
'subdir': ''
};
// Lookup the possible Dockerfile locations.
$scope.locations = null;
if (repo) {
ApiService.listBuildTriggerSubdirs($scope.trigger['config'], params).then(function(resp) {
if (resp['status'] == 'error') {
$scope.locationError = resp['message'] || 'Could not load Dockerfile locations';
$scope.locations = null;
$scope.trigger.$ready = false;
$scope.isInvalidLocation = false;
$scope.analyze({'isValid': false});
return;
}
$scope.locationError = null;
$scope.locations = resp['subdir'] || [];
$scope.trigger.$ready = true;
if ($scope.locations.length > 0) {
$scope.setLocation($scope.locations[0]);
} else {
$scope.currentLocation = null;
$scope.isInvalidLocation = resp['subdir'].indexOf('') < 0;
$scope.analyze({'isValid': !$scope.isInvalidLocation});
}
}, function(resp) {
$scope.locationError = resp['message'] || 'Could not load Dockerfile locations';
$scope.locations = null;
$scope.trigger.$ready = false;
$scope.isInvalidLocation = false;
$scope.analyze({'isValid': false});
});
}
};
var setupTypeahead = function() {
var repos = [];
for (var i = 0; i < $scope.orgs.length; ++i) {
var org = $scope.orgs[i];
var orepos = org['repos'];
for (var j = 0; j < orepos.length; ++j) {
var repoValue = {
'repo': orepos[j],
'avatar_url': org['info']['avatar_url'],
'toString': function() {
return this.repo;
}
};
var datum = {
'name': orepos[j],
'org': org,
'value': orepos[j],
'title': orepos[j],
'item': repoValue
};
repos.push(datum);
}
}
$scope.repoLookahead = repos;
};
var loadSources = function() {
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'trigger_uuid': $scope.trigger.id
};
ApiService.listTriggerBuildSources(null, params).then(function(resp) {
$scope.orgs = resp['sources'];
setupTypeahead();
$scope.loading = false;
});
};
var check = function() {
if ($scope.repository && $scope.trigger) {
loadSources();
}
};
$scope.$watch('repository', check);
$scope.$watch('trigger', check);
$scope.$watch('currentRepo', function(repo) {
$scope.selectRepoInternal(repo);
});
}
};
return directiveDefinitionObject;
});
quayApp.directive('buildLogCommand', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/build-log-command.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'command': '=command'
},
controller: function($scope, $element) {
$scope.getWithoutStep = function(fullTitle) {
var colon = fullTitle.indexOf(':');
if (colon <= 0) {
return '';
}
return $.trim(fullTitle.substring(colon + 1));
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('dockerfileCommand', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/dockerfile-command.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'command': '=command'
},
controller: function($scope, $element, UtilService, Config) {
var registryHandlers = {
'quay.io': function(pieces) {
var rnamespace = pieces[pieces.length - 2];
var rname = pieces[pieces.length - 1].split(':')[0];
return '/repository/' + rnamespace + '/' + rname + '/';
},
'': function(pieces) {
var rnamespace = pieces.length == 1 ? '_' : 'u/' + pieces[0];
var rname = pieces[pieces.length - 1].split(':')[0];
return 'https://registry.hub.docker.com/' + rnamespace + '/' + rname + '/';
}
};
registryHandlers[Config.getDomain()] = registryHandlers['quay.io'];
var kindHandlers = {
'FROM': function(title) {
var pieces = title.split('/');
var registry = pieces.length < 3 ? '' : pieces[0];
if (!registryHandlers[registry]) {
return title;
}
return '<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 'pushing':
return 'Pushing image built from Dockerfile';
case 'complete':
return 'Dockerfile build completed and pushed';
case 'error':
return 'Dockerfile build failed';
}
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('buildProgress', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/build-progress.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'build': '=build'
},
controller: function($scope, $element) {
$scope.getPercentage = function(buildInfo) {
switch (buildInfo.phase) {
case 'pulling':
return buildInfo.status.pull_completion * 100;
break;
case 'building':
return (buildInfo.status.current_command / buildInfo.status.total_commands) * 100;
break;
case 'pushing':
return buildInfo.status.push_completion * 100;
break;
case 'complete':
return 100;
break;
case 'initializing':
case 'starting':
case 'waiting':
case 'cannot_load':
case 'unpacking':
return 0;
break;
}
return -1;
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('externalNotificationView', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/external-notification-view.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'repository': '=repository',
'notification': '=notification',
'notificationDeleted': '&notificationDeleted'
},
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': '&notificationCreated'
},
controller: function($scope, $element, ExternalNotificationData, ApiService, $timeout, StringBuilderService) {
$scope.currentEvent = null;
$scope.currentMethod = null;
$scope.status = '';
$scope.currentConfig = {};
$scope.clearCounter = 0;
$scope.unauthorizedEmail = false;
$scope.events = ExternalNotificationData.getSupportedEvents();
$scope.methods = ExternalNotificationData.getSupportedMethods();
$scope.setEvent = function(event) {
$scope.currentEvent = event;
};
$scope.setMethod = function(method) {
$scope.currentConfig = {};
$scope.currentMethod = method;
$scope.unauthorizedEmail = false;
};
$scope.createNotification = function() {
if (!$scope.currentConfig.email) {
$scope.performCreateNotification();
return;
}
$scope.status = 'checking-email';
$scope.checkEmailAuthorization();
};
$scope.checkEmailAuthorization = function() {
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'email': $scope.currentConfig.email
};
ApiService.checkRepoEmailAuthorized(null, params).then(function(resp) {
$scope.handleEmailCheck(resp.confirmed);
}, function(resp) {
$scope.handleEmailCheck(false);
});
};
$scope.performCreateNotification = function() {
$scope.status = 'creating';
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name
};
var data = {
'event': $scope.currentEvent.id,
'method': $scope.currentMethod.id,
'config': $scope.currentConfig
};
ApiService.createRepoNotification(data, params).then(function(resp) {
$scope.status = '';
$scope.notificationCreated({'notification': resp});
$('#createNotificationModal').modal('hide');
});
};
$scope.handleEmailCheck = function(isAuthorized) {
if (isAuthorized) {
$scope.performCreateNotification();
return;
}
if ($scope.status == 'authorizing-email-sent') {
$scope.watchEmail();
} else {
$scope.status = 'unauthorized-email';
}
$scope.unauthorizedEmail = true;
};
$scope.sendAuthEmail = function() {
$scope.status = 'authorizing-email';
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'email': $scope.currentConfig.email
};
ApiService.sendAuthorizeRepoEmail(null, params).then(function(resp) {
$scope.status = 'authorizing-email-sent';
$scope.watchEmail();
});
};
$scope.watchEmail = function() {
// TODO: change this to SSE?
$timeout(function() {
$scope.checkEmailAuthorization();
}, 1000);
};
$scope.getHelpUrl = function(field, config) {
var helpUrl = field['help_url'];
if (!helpUrl) {
return null;
}
return StringBuilderService.buildUrl(helpUrl, config);
};
$scope.$watch('counter', function(counter) {
if (counter) {
$scope.clearCounter++;
$scope.status = '';
$scope.currentEvent = null;
$scope.currentMethod = null;
$scope.unauthorizedEmail = false;
$('#createNotificationModal').modal({});
}
});
}
};
return directiveDefinitionObject;
});
quayApp.directive('twitterView', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/twitter-view.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'avatarUrl': '@avatarUrl',
'authorName': '@authorName',
'authorUser': '@authorUser',
'messageUrl': '@messageUrl',
'messageDate': '@messageDate'
},
controller: function($scope, $element) {
}
};
return directiveDefinitionObject;
});
quayApp.directive('notificationsBubble', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/notifications-bubble.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
},
controller: function($scope, UserService, NotificationService) {
$scope.notificationService = NotificationService;
}
};
return directiveDefinitionObject;
});
quayApp.directive('notificationView', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/notification-view.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'notification': '=notification',
'parent': '=parent'
},
controller: function($scope, $element, $window, $location, UserService, NotificationService, ApiService) {
var stringStartsWith = function (str, prefix) {
return str.slice(0, prefix.length) == prefix;
};
$scope.getMessage = function(notification) {
return NotificationService.getMessage(notification);
};
$scope.getGravatar = function(orgname) {
var organization = UserService.getOrganization(orgname);
return organization['gravatar'] || '';
};
$scope.parseDate = function(dateString) {
return Date.parse(dateString);
};
$scope.showNotification = function() {
var url = NotificationService.getPage($scope.notification);
if (url) {
if (stringStartsWith(url, 'http://') || stringStartsWith(url, 'https://')) {
$window.location.href = url;
} else {
var parts = url.split('?')
$location.path(parts[0]);
if (parts.length > 1) {
$location.search(parts[1]);
}
$scope.parent.$hide();
}
}
};
$scope.dismissNotification = function(notification) {
NotificationService.dismissNotification(notification);
};
$scope.canDismiss = function(notification) {
return NotificationService.canDismiss(notification);
};
$scope.getClass = function(notification) {
return NotificationService.getClass(notification);
};
$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('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.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;
};
}]);