Merge branch 'master' into star
This commit is contained in:
commit
917dd6b674
229 changed files with 10807 additions and 3003 deletions
268
static/js/app.js
268
static/js/app.js
|
@ -129,7 +129,7 @@ function getMarkedDown(string) {
|
|||
|
||||
quayDependencies = ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment',
|
||||
'mgcrea.ngStrap', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml',
|
||||
'ngAnimate'];
|
||||
'ngAnimate', 'core-ui', 'core-config-setup'];
|
||||
|
||||
if (window.__config && window.__config.MIXPANEL_KEY) {
|
||||
quayDependencies.push('angulartics');
|
||||
|
@ -980,7 +980,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
return resource;
|
||||
};
|
||||
|
||||
var buildUrl = function(path, parameters) {
|
||||
var buildUrl = function(path, parameters, opt_forcessl) {
|
||||
// We already have /api/v1/ on the URLs, so remove them from the paths.
|
||||
path = path.substr('/api/v1/'.length, path.length);
|
||||
|
||||
|
@ -1020,6 +1020,11 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
}
|
||||
}
|
||||
|
||||
// If we are forcing SSL, return an absolutel URL with an SSL prefix.
|
||||
if (opt_forcessl) {
|
||||
path = 'https://' + window.location.host + '/api/v1/' + path;
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
|
@ -1050,12 +1055,35 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
}
|
||||
};
|
||||
|
||||
var freshLoginInProgress = [];
|
||||
var reject = function(msg) {
|
||||
for (var i = 0; i < freshLoginInProgress.length; ++i) {
|
||||
freshLoginInProgress[i].deferred.reject({'data': {'message': msg}});
|
||||
}
|
||||
freshLoginInProgress = [];
|
||||
};
|
||||
|
||||
var retry = function() {
|
||||
for (var i = 0; i < freshLoginInProgress.length; ++i) {
|
||||
freshLoginInProgress[i].retry();
|
||||
}
|
||||
freshLoginInProgress = [];
|
||||
};
|
||||
|
||||
var freshLoginFailCheck = function(opName, opArgs) {
|
||||
return function(resp) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
// If the error is a fresh login required, show the dialog.
|
||||
if (resp.status == 401 && resp.data['error_type'] == 'fresh_login_required') {
|
||||
var retryOperation = function() {
|
||||
apiService[opName].apply(apiService, opArgs).then(function(resp) {
|
||||
deferred.resolve(resp);
|
||||
}, function(resp) {
|
||||
deferred.reject(resp);
|
||||
});
|
||||
};
|
||||
|
||||
var verifyNow = function() {
|
||||
var info = {
|
||||
'password': $('#freshPassword').val()
|
||||
|
@ -1065,19 +1093,27 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
|
||||
// Conduct the sign in of the user.
|
||||
apiService.verifyUser(info).then(function() {
|
||||
// On success, retry the operation. if it succeeds, then resolve the
|
||||
// On success, retry the operations. 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);
|
||||
});
|
||||
retry();
|
||||
}, function(resp) {
|
||||
// Reject with the sign in error.
|
||||
deferred.reject({'data': {'message': 'Invalid verification credentials'}});
|
||||
reject('Invalid verification credentials');
|
||||
});
|
||||
};
|
||||
|
||||
// Add the retry call to the in progress list. If there is more than a single
|
||||
// in progress call, we skip showing the dialog (since it has already been
|
||||
// shown).
|
||||
freshLoginInProgress.push({
|
||||
'deferred': deferred,
|
||||
'retry': retryOperation
|
||||
})
|
||||
|
||||
if (freshLoginInProgress.length > 1) {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
var box = bootbox.dialog({
|
||||
"message": 'It has been more than a few minutes since you last logged in, ' +
|
||||
'so please verify your password to perform this sensitive operation:' +
|
||||
|
@ -1095,7 +1131,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
"label": "Cancel",
|
||||
"className": "btn-default",
|
||||
"callback": function() {
|
||||
deferred.reject({'data': {'message': 'Verification canceled'}});
|
||||
reject('Verification canceled')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1127,8 +1163,8 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
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));
|
||||
apiService[operationName] = function(opt_options, opt_parameters, opt_background, opt_forcessl) {
|
||||
var one = Restangular.one(buildUrl(path, opt_parameters, opt_forcessl));
|
||||
if (opt_background) {
|
||||
one.withHttpConfig({
|
||||
'ignoreLoadingBar': true
|
||||
|
@ -1247,6 +1283,39 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
return cookieService;
|
||||
}]);
|
||||
|
||||
$provide.factory('ContainerService', ['ApiService', '$timeout',
|
||||
function(ApiService, $timeout) {
|
||||
var containerService = {};
|
||||
containerService.restartContainer = function(callback) {
|
||||
ApiService.scShutdownContainer(null, null).then(function(resp) {
|
||||
$timeout(callback, 2000);
|
||||
}, ApiService.errorDisplay('Cannot restart container. Please report this to support.'))
|
||||
};
|
||||
|
||||
containerService.scheduleStatusCheck = function(callback) {
|
||||
$timeout(function() {
|
||||
containerService.checkStatus(callback);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
containerService.checkStatus = function(callback, force_ssl) {
|
||||
var errorHandler = function(resp) {
|
||||
if (resp.status == 404 || resp.status == 502) {
|
||||
// Container has not yet come back up, so we schedule another check.
|
||||
containerService.scheduleStatusCheck(callback);
|
||||
return;
|
||||
}
|
||||
|
||||
return ApiService.errorDisplay('Cannot load status. Please report this to support')(resp);
|
||||
};
|
||||
|
||||
ApiService.scRegistryStatus(null, null)
|
||||
.then(callback, errorHandler, /* background */true, /* force ssl*/force_ssl);
|
||||
};
|
||||
|
||||
return containerService;
|
||||
}]);
|
||||
|
||||
$provide.factory('UserService', ['ApiService', 'CookieService', '$rootScope', 'Config',
|
||||
function(ApiService, CookieService, $rootScope, Config) {
|
||||
var userResponse = {
|
||||
|
@ -1487,15 +1556,12 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
'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'
|
||||
'name': 'url',
|
||||
'type': 'regex',
|
||||
'title': 'Webhook URL',
|
||||
'regex': '^https://hooks\\.slack\\.com/services/[A-Z0-9]+/[A-Z0-9]+/[a-zA-Z0-9]+$',
|
||||
'help_url': 'https://slack.com/services/new/incoming-webhook',
|
||||
'placeholder': 'https://hooks.slack.com/service/{some}/{token}/{here}'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -2231,8 +2297,10 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
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('/superuser/', {title: 'Enterprise Registry Management', description:'Admin panel for ' + title, templateUrl: '/static/partials/super-user.html',
|
||||
reloadOnSearch: false, controller: SuperUserAdminCtrl, newLayout: true}).
|
||||
when('/setup/', {title: 'Enterprise Registry Setup', description:'Setup for ' + title, templateUrl: '/static/partials/setup.html',
|
||||
reloadOnSearch: false, controller: SetupCtrl, newLayout: true}).
|
||||
when('/guide/', {title: 'Guide', description:'Guide to using private docker repositories on ' + title,
|
||||
templateUrl: '/static/partials/guide.html',
|
||||
controller: GuideCtrl}).
|
||||
|
@ -2652,7 +2720,7 @@ quayApp.directive('focusablePopoverContent', ['$timeout', '$popover', function (
|
|||
|
||||
if (!scope) { return; }
|
||||
scope.$apply(function() {
|
||||
if (!scope) { return; }
|
||||
if (!scope || !$scope.$hide) { return; }
|
||||
scope.$hide();
|
||||
});
|
||||
};
|
||||
|
@ -3250,7 +3318,11 @@ quayApp.directive('logsView', function () {
|
|||
}
|
||||
|
||||
if (metadata.token) {
|
||||
prefix += ' via token {token}';
|
||||
if (metadata.token_type == 'build-worker') {
|
||||
prefix += ' by <b>build worker</b>';
|
||||
} else {
|
||||
prefix += ' via token';
|
||||
}
|
||||
} else if (metadata.username) {
|
||||
prefix += ' by {username}';
|
||||
} else {
|
||||
|
@ -3261,7 +3333,13 @@ quayApp.directive('logsView', function () {
|
|||
},
|
||||
'pull_repo': function(metadata) {
|
||||
if (metadata.token) {
|
||||
return 'Pull repository {repo} via token {token}';
|
||||
var prefix = 'Pull of repository'
|
||||
if (metadata.token_type == 'build-worker') {
|
||||
prefix += ' by <b>build worker</b>';
|
||||
} else {
|
||||
prefix += ' via token';
|
||||
}
|
||||
return prefix;
|
||||
} else if (metadata.username) {
|
||||
return 'Pull repository {repo} by {username}';
|
||||
} else {
|
||||
|
@ -3915,9 +3993,11 @@ quayApp.directive('registryName', function () {
|
|||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {},
|
||||
scope: {
|
||||
'isShort': '=isShort'
|
||||
},
|
||||
controller: function($scope, $element, Config) {
|
||||
$scope.name = Config.REGISTRY_TITLE;
|
||||
$scope.name = $scope.isShort ? Config.REGISTRY_TITLE_SHORT : Config.REGISTRY_TITLE;
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
|
@ -4076,7 +4156,7 @@ quayApp.directive('headerBar', function () {
|
|||
restrict: 'C',
|
||||
scope: {
|
||||
},
|
||||
controller: function($scope, $element, $location, UserService, PlanService, ApiService, NotificationService) {
|
||||
controller: function($scope, $element, $location, UserService, PlanService, ApiService, NotificationService, Config) {
|
||||
$scope.notificationService = NotificationService;
|
||||
|
||||
// Monitor any user changes and place the current user into the scope.
|
||||
|
@ -4095,6 +4175,14 @@ quayApp.directive('headerBar', function () {
|
|||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
$scope.getEnterpriseLogo = function() {
|
||||
if (!Config.ENTERPRISE_LOGO_URL) {
|
||||
return '/static/img/quay-logo.png';
|
||||
}
|
||||
|
||||
return Config.ENTERPRISE_LOGO_URL;
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
|
@ -4353,6 +4441,8 @@ quayApp.directive('entitySearch', function () {
|
|||
|
||||
if (classes.length > 1) {
|
||||
classes[classes.length - 1] = 'or ' + classes[classes.length - 1];
|
||||
} else if (classes.length == 0) {
|
||||
return '<div class="tt-empty">No matching entities found</div>';
|
||||
}
|
||||
|
||||
var class_string = '';
|
||||
|
@ -4438,7 +4528,6 @@ quayApp.directive('entitySearch', function () {
|
|||
|
||||
$scope.$watch('namespace', function(namespace) {
|
||||
if (!namespace) { return; }
|
||||
|
||||
$scope.isAdmin = UserService.isNamespaceAdmin(namespace);
|
||||
$scope.isOrganization = !!UserService.getOrganization(namespace);
|
||||
});
|
||||
|
@ -5749,9 +5838,15 @@ quayApp.directive('buildMessage', function () {
|
|||
case 'building':
|
||||
return 'Building image from Dockerfile';
|
||||
|
||||
case 'checking-cache':
|
||||
return 'Looking up cached images';
|
||||
|
||||
case 'priming-cache':
|
||||
return 'Priming cache for build';
|
||||
|
||||
case 'build-scheduled':
|
||||
return 'Preparing build node';
|
||||
|
||||
case 'pushing':
|
||||
return 'Pushing image built from Dockerfile';
|
||||
|
||||
|
@ -5805,6 +5900,7 @@ quayApp.directive('buildProgress', function () {
|
|||
break;
|
||||
|
||||
case 'initializing':
|
||||
case 'checking-cache':
|
||||
case 'starting':
|
||||
case 'waiting':
|
||||
case 'cannot_load':
|
||||
|
@ -5901,6 +5997,10 @@ quayApp.directive('createExternalNotificationDialog', function () {
|
|||
$scope.events = ExternalNotificationData.getSupportedEvents();
|
||||
$scope.methods = ExternalNotificationData.getSupportedMethods();
|
||||
|
||||
$scope.getPattern = function(field) {
|
||||
return new RegExp(field.regex);
|
||||
};
|
||||
|
||||
$scope.setEvent = function(event) {
|
||||
$scope.currentEvent = event;
|
||||
};
|
||||
|
@ -6224,7 +6324,7 @@ quayApp.directive('dockerfileBuildForm', function () {
|
|||
scope: {
|
||||
'repository': '=repository',
|
||||
'startNow': '=startNow',
|
||||
'hasDockerfile': '=hasDockerfile',
|
||||
'isReady': '=isReady',
|
||||
'uploadFailed': '&uploadFailed',
|
||||
'uploadStarted': '&uploadStarted',
|
||||
'buildStarted': '&buildStarted',
|
||||
|
@ -6235,6 +6335,8 @@ quayApp.directive('dockerfileBuildForm', function () {
|
|||
},
|
||||
controller: function($scope, $element, ApiService) {
|
||||
$scope.internal = {'hasDockerfile': false};
|
||||
$scope.pull_entity = null;
|
||||
$scope.is_public = true;
|
||||
|
||||
var handleBuildFailed = function(message) {
|
||||
message = message || 'Dockerfile build failed to start';
|
||||
|
@ -6308,8 +6410,12 @@ quayApp.directive('dockerfileBuildForm', function () {
|
|||
'file_id': fileId
|
||||
};
|
||||
|
||||
if (!$scope.is_public && $scope.pull_entity) {
|
||||
data['pull_robot'] = $scope.pull_entity['name'];
|
||||
}
|
||||
|
||||
var params = {
|
||||
'repository': repo.namespace + '/' + repo.name
|
||||
'repository': repo.namespace + '/' + repo.name,
|
||||
};
|
||||
|
||||
ApiService.requestRepoBuild(data, params).then(function(resp) {
|
||||
|
@ -6387,9 +6493,13 @@ quayApp.directive('dockerfileBuildForm', function () {
|
|||
});
|
||||
};
|
||||
|
||||
$scope.$watch('internal.hasDockerfile', function(d) {
|
||||
$scope.hasDockerfile = d;
|
||||
});
|
||||
var checkIsReady = function() {
|
||||
$scope.isReady = $scope.internal.hasDockerfile && ($scope.is_public || $scope.pull_entity);
|
||||
};
|
||||
|
||||
$scope.$watch('pull_entity', checkIsReady);
|
||||
$scope.$watch('is_public', checkIsReady);
|
||||
$scope.$watch('internal.hasDockerfile', checkIsReady);
|
||||
|
||||
$scope.$watch('startNow', function() {
|
||||
if ($scope.startNow && $scope.repository && !$scope.uploading && !$scope.building) {
|
||||
|
@ -6506,6 +6616,54 @@ quayApp.directive('locationView', function () {
|
|||
});
|
||||
|
||||
|
||||
quayApp.directive('psUsageGraph', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/ps-usage-graph.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'isEnabled': '=isEnabled'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.counter = -1;
|
||||
$scope.data = null;
|
||||
|
||||
var source = null;
|
||||
|
||||
var connect = function() {
|
||||
if (source) { return; }
|
||||
source = new EventSource('/realtime/ps');
|
||||
source.onmessage = function(e) {
|
||||
$scope.$apply(function() {
|
||||
$scope.counter++;
|
||||
$scope.data = JSON.parse(e.data);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
var disconnect = function() {
|
||||
if (!source) { return; }
|
||||
source.close();
|
||||
source = null;
|
||||
};
|
||||
|
||||
$scope.$watch('isEnabled', function(value) {
|
||||
if (value) {
|
||||
connect();
|
||||
} else {
|
||||
disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on("$destroy", disconnect);
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('avatar', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
|
@ -6684,6 +6842,7 @@ quayApp.directive('ngBlur', function() {
|
|||
};
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive("filePresent", [function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
|
@ -6757,7 +6916,6 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
|
|||
|
||||
var changeTab = function(activeTab, opt_timeout) {
|
||||
var checkCount = 0;
|
||||
|
||||
$timeout(function() {
|
||||
if (checkCount > 5) { return; }
|
||||
checkCount++;
|
||||
|
@ -6821,6 +6979,8 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
|
|||
$rootScope.pageClass = current.$$route.pageClass;
|
||||
}
|
||||
|
||||
$rootScope.newLayout = !!current.$$route.newLayout;
|
||||
|
||||
if (current.$$route.description) {
|
||||
$rootScope.description = current.$$route.description;
|
||||
} else {
|
||||
|
@ -6836,26 +6996,28 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
|
|||
|
||||
// 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;
|
||||
}
|
||||
$timeout(function() {
|
||||
$('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);
|
||||
$location.search(newSearch);
|
||||
});
|
||||
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
if (activeTab) {
|
||||
changeTab(activeTab);
|
||||
}
|
||||
if (activeTab) {
|
||||
changeTab(activeTab);
|
||||
}
|
||||
}, 400); // 400ms to make sure angular has rendered.
|
||||
});
|
||||
|
||||
var initallyChecked = false;
|
||||
|
|
|
@ -1011,257 +1011,6 @@ function BuildPackageCtrl($scope, Restangular, ApiService, DataFileService, $rou
|
|||
getBuildInfo();
|
||||
}
|
||||
|
||||
function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $interval, $sanitize,
|
||||
ansi2html, AngularViewArray, AngularPollChannel) {
|
||||
var namespace = $routeParams.namespace;
|
||||
var name = $routeParams.name;
|
||||
|
||||
// Watch for changes to the current parameter.
|
||||
$scope.$on('$routeUpdate', function(){
|
||||
if ($location.search().current) {
|
||||
$scope.setCurrentBuild($location.search().current, false);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.builds = null;
|
||||
$scope.pollChannel = null;
|
||||
$scope.buildDialogShowCounter = 0;
|
||||
|
||||
$scope.showNewBuildDialog = function() {
|
||||
$scope.buildDialogShowCounter++;
|
||||
};
|
||||
|
||||
$scope.handleBuildStarted = function(newBuild) {
|
||||
if (!$scope.builds) { return; }
|
||||
|
||||
$scope.builds.unshift(newBuild);
|
||||
$scope.setCurrentBuild(newBuild['id'], true);
|
||||
};
|
||||
|
||||
$scope.adjustLogHeight = function() {
|
||||
var triggerOffset = 0;
|
||||
if ($scope.currentBuild && $scope.currentBuild.trigger) {
|
||||
triggerOffset = 85;
|
||||
}
|
||||
$('.build-logs').height($(window).height() - 415 - triggerOffset);
|
||||
};
|
||||
|
||||
$scope.askRestartBuild = function(build) {
|
||||
$('#confirmRestartBuildModal').modal({});
|
||||
};
|
||||
|
||||
$scope.restartBuild = function(build) {
|
||||
$('#confirmRestartBuildModal').modal('hide');
|
||||
|
||||
var subdirectory = '';
|
||||
if (build['job_config']) {
|
||||
subdirectory = build['job_config']['build_subdir'] || '';
|
||||
}
|
||||
|
||||
var data = {
|
||||
'file_id': build['resource_key'],
|
||||
'subdirectory': subdirectory,
|
||||
'docker_tags': build['job_config']['docker_tags']
|
||||
};
|
||||
|
||||
if (build['pull_robot']) {
|
||||
data['pull_robot'] = build['pull_robot']['name'];
|
||||
}
|
||||
|
||||
var params = {
|
||||
'repository': namespace + '/' + name
|
||||
};
|
||||
|
||||
ApiService.requestRepoBuild(data, params).then(function(newBuild) {
|
||||
if (!$scope.builds) { return; }
|
||||
|
||||
$scope.builds.unshift(newBuild);
|
||||
$scope.setCurrentBuild(newBuild['id'], true);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.hasLogs = function(container) {
|
||||
return container.logs.hasEntries;
|
||||
};
|
||||
|
||||
$scope.setCurrentBuild = function(buildId, opt_updateURL) {
|
||||
if (!$scope.builds) { return; }
|
||||
|
||||
// Find the build.
|
||||
for (var i = 0; i < $scope.builds.length; ++i) {
|
||||
if ($scope.builds[i].id == buildId) {
|
||||
$scope.setCurrentBuildInternal(i, $scope.builds[i], opt_updateURL);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.processANSI = function(message, container) {
|
||||
var filter = container.logs._filter = (container.logs._filter || ansi2html.create());
|
||||
|
||||
// Note: order is important here.
|
||||
var setup = filter.getSetupHtml();
|
||||
var stream = filter.addInputToStream(message);
|
||||
var teardown = filter.getTeardownHtml();
|
||||
return setup + stream + teardown;
|
||||
};
|
||||
|
||||
$scope.setCurrentBuildInternal = function(index, build, opt_updateURL) {
|
||||
if (build == $scope.currentBuild) { return; }
|
||||
|
||||
$scope.logEntries = null;
|
||||
$scope.logStartIndex = null;
|
||||
$scope.currentParentEntry = null;
|
||||
|
||||
$scope.currentBuild = build;
|
||||
|
||||
if (opt_updateURL) {
|
||||
if (build) {
|
||||
$location.search('current', build.id);
|
||||
} else {
|
||||
$location.search('current', null);
|
||||
}
|
||||
}
|
||||
|
||||
// Timeout needed to ensure the log element has been created
|
||||
// before its height is adjusted.
|
||||
setTimeout(function() {
|
||||
$scope.adjustLogHeight();
|
||||
}, 1);
|
||||
|
||||
// Stop any existing polling.
|
||||
if ($scope.pollChannel) {
|
||||
$scope.pollChannel.stop();
|
||||
}
|
||||
|
||||
// Create a new channel for polling the build status and logs.
|
||||
var conductStatusAndLogRequest = function(callback) {
|
||||
getBuildStatusAndLogs(build, callback);
|
||||
};
|
||||
|
||||
$scope.pollChannel = AngularPollChannel.create($scope, conductStatusAndLogRequest, 5 * 1000 /* 5s */);
|
||||
$scope.pollChannel.start();
|
||||
};
|
||||
|
||||
var processLogs = function(logs, startIndex, endIndex) {
|
||||
if (!$scope.logEntries) { $scope.logEntries = []; }
|
||||
|
||||
// If the start index given is less than that requested, then we've received a larger
|
||||
// pool of logs, and we need to only consider the new ones.
|
||||
if (startIndex < $scope.logStartIndex) {
|
||||
logs = logs.slice($scope.logStartIndex - startIndex);
|
||||
}
|
||||
|
||||
for (var i = 0; i < logs.length; ++i) {
|
||||
var entry = logs[i];
|
||||
var type = entry['type'] || 'entry';
|
||||
if (type == 'command' || type == 'phase' || type == 'error') {
|
||||
entry['logs'] = AngularViewArray.create();
|
||||
entry['index'] = $scope.logStartIndex + i;
|
||||
|
||||
$scope.logEntries.push(entry);
|
||||
$scope.currentParentEntry = entry;
|
||||
} else if ($scope.currentParentEntry) {
|
||||
$scope.currentParentEntry['logs'].push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
return endIndex;
|
||||
};
|
||||
|
||||
var getBuildStatusAndLogs = function(build, callback) {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name,
|
||||
'build_uuid': build.id
|
||||
};
|
||||
|
||||
ApiService.getRepoBuildStatus(null, params, true).then(function(resp) {
|
||||
if (build != $scope.currentBuild) { callback(false); return; }
|
||||
|
||||
// Note: We use extend here rather than replacing as Angular is depending on the
|
||||
// root build object to remain the same object.
|
||||
var matchingBuilds = $.grep($scope.builds, function(elem) {
|
||||
return elem['id'] == resp['id']
|
||||
});
|
||||
|
||||
var currentBuild = matchingBuilds.length > 0 ? matchingBuilds[0] : null;
|
||||
if (currentBuild) {
|
||||
currentBuild = $.extend(true, currentBuild, resp);
|
||||
} else {
|
||||
currentBuild = resp;
|
||||
$scope.builds.push(currentBuild);
|
||||
}
|
||||
|
||||
// Load the updated logs for the build.
|
||||
var options = {
|
||||
'start': $scope.logStartIndex
|
||||
};
|
||||
|
||||
ApiService.getRepoBuildLogsAsResource(params, true).withOptions(options).get(function(resp) {
|
||||
if (build != $scope.currentBuild) { callback(false); return; }
|
||||
|
||||
// Process the logs we've received.
|
||||
$scope.logStartIndex = processLogs(resp['logs'], resp['start'], resp['total']);
|
||||
|
||||
// If the build status is an error, open the last two log entries.
|
||||
if (currentBuild['phase'] == 'error' && $scope.logEntries.length > 1) {
|
||||
var openLogEntries = function(entry) {
|
||||
if (entry.logs) {
|
||||
entry.logs.setVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
openLogEntries($scope.logEntries[$scope.logEntries.length - 2]);
|
||||
openLogEntries($scope.logEntries[$scope.logEntries.length - 1]);
|
||||
}
|
||||
|
||||
// If the build phase is an error or a complete, then we mark the channel
|
||||
// as closed.
|
||||
callback(currentBuild['phase'] != 'error' && currentBuild['phase'] != 'complete');
|
||||
}, function() {
|
||||
callback(false);
|
||||
});
|
||||
}, function() {
|
||||
callback(false);
|
||||
});
|
||||
};
|
||||
|
||||
var fetchRepository = function() {
|
||||
var params = {'repository': namespace + '/' + name};
|
||||
$rootScope.title = 'Loading Repository...';
|
||||
$scope.repository = ApiService.getRepoAsResource(params).get(function(repo) {
|
||||
if (!repo.can_write) {
|
||||
$rootScope.title = 'Unknown builds';
|
||||
$scope.accessDenied = true;
|
||||
return;
|
||||
}
|
||||
|
||||
$rootScope.title = 'Repository Builds';
|
||||
$scope.repo = repo;
|
||||
|
||||
getBuildInfo();
|
||||
});
|
||||
};
|
||||
|
||||
var getBuildInfo = function(repo) {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name
|
||||
};
|
||||
|
||||
ApiService.getRepoBuilds(null, params).then(function(resp) {
|
||||
$scope.builds = resp.builds;
|
||||
|
||||
if ($location.search().current) {
|
||||
$scope.setCurrentBuild($location.search().current, false);
|
||||
} else if ($scope.builds.length > 0) {
|
||||
$scope.setCurrentBuild($scope.builds[0].id, true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
fetchRepository();
|
||||
}
|
||||
|
||||
function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, TriggerService, $routeParams,
|
||||
$rootScope, $location, UserService, Config, Features, ExternalNotificationData) {
|
||||
|
||||
|
@ -2748,138 +2497,6 @@ function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $tim
|
|||
loadApplicationInfo();
|
||||
}
|
||||
|
||||
|
||||
function SuperUserAdminCtrl($scope, ApiService, Features, UserService) {
|
||||
if (!Features.SUPER_USERS) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Monitor any user changes and place the current user into the scope.
|
||||
UserService.updateUserIn($scope);
|
||||
|
||||
$scope.logsCounter = 0;
|
||||
$scope.newUser = {};
|
||||
$scope.createdUsers = [];
|
||||
$scope.systemUsage = null;
|
||||
|
||||
$scope.getUsage = function() {
|
||||
if ($scope.systemUsage) { return; }
|
||||
|
||||
ApiService.getSystemUsage().then(function(resp) {
|
||||
$scope.systemUsage = resp;
|
||||
}, ApiService.errorDisplay('Cannot load system usage. Please contact support.'))
|
||||
}
|
||||
|
||||
$scope.loadLogs = function() {
|
||||
$scope.logsCounter++;
|
||||
};
|
||||
|
||||
$scope.loadUsers = function() {
|
||||
if ($scope.users) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.loadUsersInternal();
|
||||
};
|
||||
|
||||
$scope.loadUsersInternal = function() {
|
||||
ApiService.listAllUsers().then(function(resp) {
|
||||
$scope.users = resp['users'];
|
||||
$scope.showInterface = true;
|
||||
}, function(resp) {
|
||||
$scope.users = [];
|
||||
$scope.usersError = resp['data']['message'] || resp['data']['error_description'];
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showChangePassword = function(user) {
|
||||
$scope.userToChange = user;
|
||||
$('#changePasswordModal').modal({});
|
||||
};
|
||||
|
||||
$scope.createUser = function() {
|
||||
$scope.creatingUser = true;
|
||||
var errorHandler = ApiService.errorDisplay('Cannot create user', function() {
|
||||
$scope.creatingUser = false;
|
||||
});
|
||||
|
||||
ApiService.createInstallUser($scope.newUser, null).then(function(resp) {
|
||||
$scope.creatingUser = false;
|
||||
$scope.newUser = {};
|
||||
$scope.createdUsers.push(resp);
|
||||
}, errorHandler)
|
||||
};
|
||||
|
||||
$scope.showDeleteUser = function(user) {
|
||||
if (user.username == UserService.currentUser().username) {
|
||||
bootbox.dialog({
|
||||
"message": 'Cannot delete yourself!',
|
||||
"title": "Cannot delete user",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.userToDelete = user;
|
||||
$('#confirmDeleteUserModal').modal({});
|
||||
};
|
||||
|
||||
$scope.changeUserPassword = function(user) {
|
||||
$('#changePasswordModal').modal('hide');
|
||||
|
||||
var params = {
|
||||
'username': user.username
|
||||
};
|
||||
|
||||
var data = {
|
||||
'password': user.password
|
||||
};
|
||||
|
||||
ApiService.changeInstallUser(data, params).then(function(resp) {
|
||||
$scope.loadUsersInternal();
|
||||
}, ApiService.errorDisplay('Could not change user'));
|
||||
};
|
||||
|
||||
$scope.deleteUser = function(user) {
|
||||
$('#confirmDeleteUserModal').modal('hide');
|
||||
|
||||
var params = {
|
||||
'username': user.username
|
||||
};
|
||||
|
||||
ApiService.deleteInstallUser(null, params).then(function(resp) {
|
||||
$scope.loadUsersInternal();
|
||||
}, ApiService.errorDisplay('Cannot delete user'));
|
||||
};
|
||||
|
||||
$scope.sendRecoveryEmail = function(user) {
|
||||
var params = {
|
||||
'username': user.username
|
||||
};
|
||||
|
||||
ApiService.sendInstallUserRecoveryEmail(null, params).then(function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": "A recovery email has been sent to " + resp['email'],
|
||||
"title": "Recovery email sent",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}, ApiService.errorDisplay('Cannot send recovery email'))
|
||||
};
|
||||
|
||||
$scope.loadUsers();
|
||||
}
|
||||
|
||||
function TourCtrl($scope, $location) {
|
||||
$scope.kind = $location.path().substring('/tour/'.length);
|
||||
}
|
||||
|
|
272
static/js/controllers/repo-build.js
Normal file
272
static/js/controllers/repo-build.js
Normal file
|
@ -0,0 +1,272 @@
|
|||
function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $interval, $sanitize,
|
||||
ansi2html, AngularViewArray, AngularPollChannel) {
|
||||
var namespace = $routeParams.namespace;
|
||||
var name = $routeParams.name;
|
||||
|
||||
// Watch for changes to the current parameter.
|
||||
$scope.$on('$routeUpdate', function(){
|
||||
if ($location.search().current) {
|
||||
$scope.setCurrentBuild($location.search().current, false);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.builds = null;
|
||||
$scope.pollChannel = null;
|
||||
$scope.buildDialogShowCounter = 0;
|
||||
|
||||
$scope.showNewBuildDialog = function() {
|
||||
$scope.buildDialogShowCounter++;
|
||||
};
|
||||
|
||||
$scope.handleBuildStarted = function(newBuild) {
|
||||
if (!$scope.builds) { return; }
|
||||
|
||||
$scope.builds.unshift(newBuild);
|
||||
$scope.setCurrentBuild(newBuild['id'], true);
|
||||
};
|
||||
|
||||
$scope.adjustLogHeight = function() {
|
||||
var triggerOffset = 0;
|
||||
if ($scope.currentBuild && $scope.currentBuild.trigger) {
|
||||
triggerOffset = 85;
|
||||
}
|
||||
$('.build-logs').height($(window).height() - 415 - triggerOffset);
|
||||
};
|
||||
|
||||
$scope.askRestartBuild = function(build) {
|
||||
$('#confirmRestartBuildModal').modal({});
|
||||
};
|
||||
|
||||
$scope.askCancelBuild = function(build) {
|
||||
bootbox.confirm('Are you sure you want to cancel this build?', function(r) {
|
||||
if (r) {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name,
|
||||
'build_uuid': build.id
|
||||
};
|
||||
|
||||
ApiService.cancelRepoBuild(null, params).then(function() {
|
||||
if (!$scope.builds) { return; }
|
||||
$scope.builds.splice($.inArray(build, $scope.builds), 1);
|
||||
|
||||
if ($scope.builds.length) {
|
||||
$scope.currentBuild = $scope.builds[0];
|
||||
} else {
|
||||
$scope.currentBuild = null;
|
||||
}
|
||||
}, ApiService.errorDisplay('Cannot cancel build'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.restartBuild = function(build) {
|
||||
$('#confirmRestartBuildModal').modal('hide');
|
||||
|
||||
var subdirectory = '';
|
||||
if (build['job_config']) {
|
||||
subdirectory = build['job_config']['build_subdir'] || '';
|
||||
}
|
||||
|
||||
var data = {
|
||||
'file_id': build['resource_key'],
|
||||
'subdirectory': subdirectory,
|
||||
'docker_tags': build['job_config']['docker_tags']
|
||||
};
|
||||
|
||||
if (build['pull_robot']) {
|
||||
data['pull_robot'] = build['pull_robot']['name'];
|
||||
}
|
||||
|
||||
var params = {
|
||||
'repository': namespace + '/' + name
|
||||
};
|
||||
|
||||
ApiService.requestRepoBuild(data, params).then(function(newBuild) {
|
||||
if (!$scope.builds) { return; }
|
||||
|
||||
$scope.builds.unshift(newBuild);
|
||||
$scope.setCurrentBuild(newBuild['id'], true);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.hasLogs = function(container) {
|
||||
return container.logs.hasEntries;
|
||||
};
|
||||
|
||||
$scope.setCurrentBuild = function(buildId, opt_updateURL) {
|
||||
if (!$scope.builds) { return; }
|
||||
|
||||
// Find the build.
|
||||
for (var i = 0; i < $scope.builds.length; ++i) {
|
||||
if ($scope.builds[i].id == buildId) {
|
||||
$scope.setCurrentBuildInternal(i, $scope.builds[i], opt_updateURL);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.processANSI = function(message, container) {
|
||||
var filter = container.logs._filter = (container.logs._filter || ansi2html.create());
|
||||
|
||||
// Note: order is important here.
|
||||
var setup = filter.getSetupHtml();
|
||||
var stream = filter.addInputToStream(message);
|
||||
var teardown = filter.getTeardownHtml();
|
||||
return setup + stream + teardown;
|
||||
};
|
||||
|
||||
$scope.setCurrentBuildInternal = function(index, build, opt_updateURL) {
|
||||
if (build == $scope.currentBuild) { return; }
|
||||
|
||||
$scope.logEntries = null;
|
||||
$scope.logStartIndex = null;
|
||||
$scope.currentParentEntry = null;
|
||||
|
||||
$scope.currentBuild = build;
|
||||
|
||||
if (opt_updateURL) {
|
||||
if (build) {
|
||||
$location.search('current', build.id);
|
||||
} else {
|
||||
$location.search('current', null);
|
||||
}
|
||||
}
|
||||
|
||||
// Timeout needed to ensure the log element has been created
|
||||
// before its height is adjusted.
|
||||
setTimeout(function() {
|
||||
$scope.adjustLogHeight();
|
||||
}, 1);
|
||||
|
||||
// Stop any existing polling.
|
||||
if ($scope.pollChannel) {
|
||||
$scope.pollChannel.stop();
|
||||
}
|
||||
|
||||
// Create a new channel for polling the build status and logs.
|
||||
var conductStatusAndLogRequest = function(callback) {
|
||||
getBuildStatusAndLogs(build, callback);
|
||||
};
|
||||
|
||||
$scope.pollChannel = AngularPollChannel.create($scope, conductStatusAndLogRequest, 5 * 1000 /* 5s */);
|
||||
$scope.pollChannel.start();
|
||||
};
|
||||
|
||||
var processLogs = function(logs, startIndex, endIndex) {
|
||||
if (!$scope.logEntries) { $scope.logEntries = []; }
|
||||
|
||||
// If the start index given is less than that requested, then we've received a larger
|
||||
// pool of logs, and we need to only consider the new ones.
|
||||
if (startIndex < $scope.logStartIndex) {
|
||||
logs = logs.slice($scope.logStartIndex - startIndex);
|
||||
}
|
||||
|
||||
for (var i = 0; i < logs.length; ++i) {
|
||||
var entry = logs[i];
|
||||
var type = entry['type'] || 'entry';
|
||||
if (type == 'command' || type == 'phase' || type == 'error') {
|
||||
entry['logs'] = AngularViewArray.create();
|
||||
entry['index'] = $scope.logStartIndex + i;
|
||||
|
||||
$scope.logEntries.push(entry);
|
||||
$scope.currentParentEntry = entry;
|
||||
} else if ($scope.currentParentEntry) {
|
||||
$scope.currentParentEntry['logs'].push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
return endIndex;
|
||||
};
|
||||
|
||||
var getBuildStatusAndLogs = function(build, callback) {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name,
|
||||
'build_uuid': build.id
|
||||
};
|
||||
|
||||
ApiService.getRepoBuildStatus(null, params, true).then(function(resp) {
|
||||
if (build != $scope.currentBuild) { callback(false); return; }
|
||||
|
||||
// Note: We use extend here rather than replacing as Angular is depending on the
|
||||
// root build object to remain the same object.
|
||||
var matchingBuilds = $.grep($scope.builds, function(elem) {
|
||||
return elem['id'] == resp['id']
|
||||
});
|
||||
|
||||
var currentBuild = matchingBuilds.length > 0 ? matchingBuilds[0] : null;
|
||||
if (currentBuild) {
|
||||
currentBuild = $.extend(true, currentBuild, resp);
|
||||
} else {
|
||||
currentBuild = resp;
|
||||
$scope.builds.push(currentBuild);
|
||||
}
|
||||
|
||||
// Load the updated logs for the build.
|
||||
var options = {
|
||||
'start': $scope.logStartIndex
|
||||
};
|
||||
|
||||
ApiService.getRepoBuildLogsAsResource(params, true).withOptions(options).get(function(resp) {
|
||||
if (build != $scope.currentBuild) { callback(false); return; }
|
||||
|
||||
// Process the logs we've received.
|
||||
$scope.logStartIndex = processLogs(resp['logs'], resp['start'], resp['total']);
|
||||
|
||||
// If the build status is an error, open the last two log entries.
|
||||
if (currentBuild['phase'] == 'error' && $scope.logEntries.length > 1) {
|
||||
var openLogEntries = function(entry) {
|
||||
if (entry.logs) {
|
||||
entry.logs.setVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
openLogEntries($scope.logEntries[$scope.logEntries.length - 2]);
|
||||
openLogEntries($scope.logEntries[$scope.logEntries.length - 1]);
|
||||
}
|
||||
|
||||
// If the build phase is an error or a complete, then we mark the channel
|
||||
// as closed.
|
||||
callback(currentBuild['phase'] != 'error' && currentBuild['phase'] != 'complete');
|
||||
}, function() {
|
||||
callback(false);
|
||||
});
|
||||
}, function() {
|
||||
callback(false);
|
||||
});
|
||||
};
|
||||
|
||||
var fetchRepository = function() {
|
||||
var params = {'repository': namespace + '/' + name};
|
||||
$rootScope.title = 'Loading Repository...';
|
||||
$scope.repository = ApiService.getRepoAsResource(params).get(function(repo) {
|
||||
if (!repo.can_write) {
|
||||
$rootScope.title = 'Unknown builds';
|
||||
$scope.accessDenied = true;
|
||||
return;
|
||||
}
|
||||
|
||||
$rootScope.title = 'Repository Builds';
|
||||
$scope.repo = repo;
|
||||
|
||||
getBuildInfo();
|
||||
});
|
||||
};
|
||||
|
||||
var getBuildInfo = function(repo) {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name
|
||||
};
|
||||
|
||||
ApiService.getRepoBuilds(null, params).then(function(resp) {
|
||||
$scope.builds = resp.builds;
|
||||
|
||||
if ($location.search().current) {
|
||||
$scope.setCurrentBuild($location.search().current, false);
|
||||
} else if ($scope.builds.length > 0) {
|
||||
$scope.setCurrentBuild($scope.builds[0].id, true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
fetchRepository();
|
||||
}
|
282
static/js/controllers/setup.js
Normal file
282
static/js/controllers/setup.js
Normal file
|
@ -0,0 +1,282 @@
|
|||
function SetupCtrl($scope, $timeout, ApiService, Features, UserService, ContainerService, CoreDialog) {
|
||||
if (!Features.SUPER_USERS) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.HOSTNAME_REGEX = '^[a-zA-Z-0-9\.]+(:[0-9]+)?$';
|
||||
|
||||
$scope.validateHostname = function(hostname) {
|
||||
if (hostname.indexOf('127.0.0.1') == 0 || hostname.indexOf('localhost') == 0) {
|
||||
return 'Please specify a non-localhost hostname. "localhost" will refer to the container, not your machine.'
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Note: The values of the enumeration are important for isStepFamily. For example,
|
||||
// *all* states under the "configuring db" family must start with "config-db".
|
||||
$scope.States = {
|
||||
// Loading the state of the product.
|
||||
'LOADING': 'loading',
|
||||
|
||||
// The configuration directory is missing.
|
||||
'MISSING_CONFIG_DIR': 'missing-config-dir',
|
||||
|
||||
// The config.yaml exists but it is invalid.
|
||||
'INVALID_CONFIG': 'config-invalid',
|
||||
|
||||
// DB is being configured.
|
||||
'CONFIG_DB': 'config-db',
|
||||
|
||||
// DB information is being validated.
|
||||
'VALIDATING_DB': 'config-db-validating',
|
||||
|
||||
// DB information is being saved to the config.
|
||||
'SAVING_DB': 'config-db-saving',
|
||||
|
||||
// A validation error occurred with the database.
|
||||
'DB_ERROR': 'config-db-error',
|
||||
|
||||
// Database is being setup.
|
||||
'DB_SETUP': 'setup-db',
|
||||
|
||||
// Database setup has succeeded.
|
||||
'DB_SETUP_SUCCESS': 'setup-db-success',
|
||||
|
||||
// An error occurred when setting up the database.
|
||||
'DB_SETUP_ERROR': 'setup-db-error',
|
||||
|
||||
// The container is being restarted for the database changes.
|
||||
'DB_RESTARTING': 'setup-db-restarting',
|
||||
|
||||
// A superuser is being configured.
|
||||
'CREATE_SUPERUSER': 'create-superuser',
|
||||
|
||||
// The superuser is being created.
|
||||
'CREATING_SUPERUSER': 'create-superuser-creating',
|
||||
|
||||
// An error occurred when setting up the superuser.
|
||||
'SUPERUSER_ERROR': 'create-superuser-error',
|
||||
|
||||
// The superuser was created successfully.
|
||||
'SUPERUSER_CREATED': 'create-superuser-created',
|
||||
|
||||
// General configuration is being setup.
|
||||
'CONFIG': 'config',
|
||||
|
||||
// The configuration is fully valid.
|
||||
'VALID_CONFIG': 'valid-config',
|
||||
|
||||
// The container is being restarted for the configuration changes.
|
||||
'CONFIG_RESTARTING': 'config-restarting',
|
||||
|
||||
// The product is ready for use.
|
||||
'READY': 'ready'
|
||||
}
|
||||
|
||||
$scope.csrf_token = window.__token;
|
||||
$scope.currentStep = $scope.States.LOADING;
|
||||
$scope.errors = {};
|
||||
$scope.stepProgress = [];
|
||||
$scope.hasSSL = false;
|
||||
$scope.hostname = null;
|
||||
|
||||
$scope.$watch('currentStep', function(currentStep) {
|
||||
$scope.stepProgress = $scope.getProgress(currentStep);
|
||||
|
||||
switch (currentStep) {
|
||||
case $scope.States.CONFIG:
|
||||
$('#setupModal').modal('hide');
|
||||
break;
|
||||
|
||||
case $scope.States.MISSING_CONFIG_DIR:
|
||||
$scope.showMissingConfigDialog();
|
||||
break;
|
||||
|
||||
case $scope.States.INVALID_CONFIG:
|
||||
$scope.showInvalidConfigDialog();
|
||||
break;
|
||||
|
||||
case $scope.States.DB_SETUP:
|
||||
$scope.performDatabaseSetup();
|
||||
// Fall-through.
|
||||
|
||||
case $scope.States.CREATE_SUPERUSER:
|
||||
case $scope.States.DB_RESTARTING:
|
||||
case $scope.States.CONFIG_DB:
|
||||
case $scope.States.VALID_CONFIG:
|
||||
case $scope.States.READY:
|
||||
$('#setupModal').modal({
|
||||
keyboard: false,
|
||||
backdrop: 'static'
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
$scope.restartContainer = function(state) {
|
||||
$scope.currentStep = state;
|
||||
ContainerService.restartContainer(function() {
|
||||
$scope.checkStatus()
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showSuperuserPanel = function() {
|
||||
$('#setupModal').modal('hide');
|
||||
var prefix = $scope.hasSSL ? 'https' : 'http';
|
||||
var hostname = $scope.hostname;
|
||||
window.location = prefix + '://' + hostname + '/superuser';
|
||||
};
|
||||
|
||||
$scope.configurationSaved = function(config) {
|
||||
$scope.hasSSL = config['PREFERRED_URL_SCHEME'] == 'https';
|
||||
$scope.hostname = config['SERVER_HOSTNAME'];
|
||||
$scope.currentStep = $scope.States.VALID_CONFIG;
|
||||
};
|
||||
|
||||
$scope.getProgress = function(step) {
|
||||
var isStep = $scope.isStep;
|
||||
var isStepFamily = $scope.isStepFamily;
|
||||
var States = $scope.States;
|
||||
|
||||
return [
|
||||
isStepFamily(step, States.CONFIG_DB),
|
||||
isStepFamily(step, States.DB_SETUP),
|
||||
isStep(step, States.DB_RESTARTING),
|
||||
isStepFamily(step, States.CREATE_SUPERUSER),
|
||||
isStep(step, States.CONFIG),
|
||||
isStep(step, States.VALID_CONFIG),
|
||||
isStep(step, States.CONFIG_RESTARTING),
|
||||
isStep(step, States.READY)
|
||||
];
|
||||
};
|
||||
|
||||
$scope.isStepFamily = function(step, family) {
|
||||
if (!step) { return false; }
|
||||
return step.indexOf(family) == 0;
|
||||
};
|
||||
|
||||
$scope.isStep = function(step) {
|
||||
for (var i = 1; i < arguments.length; ++i) {
|
||||
if (arguments[i] == step) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
$scope.showInvalidConfigDialog = function() {
|
||||
var message = "The <code>config.yaml</code> file found in <code>conf/stack</code> could not be parsed."
|
||||
var title = "Invalid configuration file";
|
||||
CoreDialog.fatal(title, message);
|
||||
};
|
||||
|
||||
|
||||
$scope.showMissingConfigDialog = function() {
|
||||
var message = "A volume should be mounted into the container at <code>/conf/stack</code>: " +
|
||||
"<br><br><pre>docker run -v /path/to/config:/conf/stack</pre>" +
|
||||
"<br>Once fixed, restart the container. For more information, " +
|
||||
"<a href='https://coreos.com/docs/enterprise-registry/initial-setup/'>" +
|
||||
"Read the Setup Guide</a>"
|
||||
|
||||
var title = "Missing configuration volume";
|
||||
CoreDialog.fatal(title, message);
|
||||
};
|
||||
|
||||
$scope.parseDbUri = function(value) {
|
||||
if (!value) { return null; }
|
||||
|
||||
// Format: mysql+pymysql://<username>:<url escaped password>@<hostname>/<database_name>
|
||||
var uri = URI(value);
|
||||
return {
|
||||
'kind': uri.protocol(),
|
||||
'username': uri.username(),
|
||||
'password': uri.password(),
|
||||
'server': uri.host(),
|
||||
'database': uri.path() ? uri.path().substr(1) : ''
|
||||
};
|
||||
};
|
||||
|
||||
$scope.serializeDbUri = function(fields) {
|
||||
if (!fields['server']) { return ''; }
|
||||
|
||||
try {
|
||||
if (!fields['server']) { return ''; }
|
||||
if (!fields['database']) { return ''; }
|
||||
|
||||
var uri = URI();
|
||||
uri = uri && uri.host(fields['server']);
|
||||
uri = uri && uri.protocol(fields['kind']);
|
||||
uri = uri && uri.username(fields['username']);
|
||||
uri = uri && uri.password(fields['password']);
|
||||
uri = uri && uri.path('/' + (fields['database'] || ''));
|
||||
uri = uri && uri.toString();
|
||||
} catch (ex) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return uri;
|
||||
};
|
||||
|
||||
$scope.createSuperUser = function() {
|
||||
$scope.currentStep = $scope.States.CREATING_SUPERUSER;
|
||||
ApiService.scCreateInitialSuperuser($scope.superUser, null).then(function(resp) {
|
||||
UserService.load();
|
||||
$scope.checkStatus();
|
||||
}, function(resp) {
|
||||
$scope.currentStep = $scope.States.SUPERUSER_ERROR;
|
||||
$scope.errors.SuperuserCreationError = ApiService.getErrorMessage(resp, 'Could not create superuser');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.performDatabaseSetup = function() {
|
||||
$scope.currentStep = $scope.States.DB_SETUP;
|
||||
ApiService.scSetupDatabase(null, null).then(function(resp) {
|
||||
if (resp['error']) {
|
||||
$scope.currentStep = $scope.States.DB_SETUP_ERROR;
|
||||
$scope.errors.DatabaseSetupError = resp['error'];
|
||||
} else {
|
||||
$scope.currentStep = $scope.States.DB_SETUP_SUCCESS;
|
||||
}
|
||||
}, ApiService.errorDisplay('Could not setup database. Please report this to support.'))
|
||||
};
|
||||
|
||||
$scope.validateDatabase = function() {
|
||||
$scope.currentStep = $scope.States.VALIDATING_DB;
|
||||
$scope.databaseInvalid = null;
|
||||
|
||||
var data = {
|
||||
'config': {
|
||||
'DB_URI': $scope.databaseUri
|
||||
},
|
||||
'hostname': window.location.host
|
||||
};
|
||||
|
||||
var params = {
|
||||
'service': 'database'
|
||||
};
|
||||
|
||||
ApiService.scValidateConfig(data, params).then(function(resp) {
|
||||
var status = resp.status;
|
||||
|
||||
if (status) {
|
||||
$scope.currentStep = $scope.States.SAVING_DB;
|
||||
ApiService.scUpdateConfig(data, null).then(function(resp) {
|
||||
$scope.checkStatus();
|
||||
}, ApiService.errorDisplay('Cannot update config. Please report this to support'));
|
||||
} else {
|
||||
$scope.currentStep = $scope.States.DB_ERROR;
|
||||
$scope.errors.DatabaseValidationError = resp.reason;
|
||||
}
|
||||
}, ApiService.errorDisplay('Cannot validate database. Please report this to support'));
|
||||
};
|
||||
|
||||
$scope.checkStatus = function() {
|
||||
ContainerService.checkStatus(function(resp) {
|
||||
$scope.currentStep = resp['status'];
|
||||
}, $scope.hasSSL);
|
||||
};
|
||||
|
||||
// Load the initial status.
|
||||
$scope.checkStatus();
|
||||
}
|
229
static/js/controllers/superuser.js
Normal file
229
static/js/controllers/superuser.js
Normal file
|
@ -0,0 +1,229 @@
|
|||
function SuperUserAdminCtrl($scope, $timeout, ApiService, Features, UserService, ContainerService, AngularPollChannel, CoreDialog) {
|
||||
if (!Features.SUPER_USERS) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Monitor any user changes and place the current user into the scope.
|
||||
UserService.updateUserIn($scope);
|
||||
|
||||
$scope.configStatus = null;
|
||||
$scope.requiresRestart = null;
|
||||
$scope.logsCounter = 0;
|
||||
$scope.newUser = {};
|
||||
$scope.createdUser = null;
|
||||
$scope.systemUsage = null;
|
||||
$scope.debugServices = null;
|
||||
$scope.debugLogs = null;
|
||||
$scope.pollChannel = null;
|
||||
$scope.logsScrolled = false;
|
||||
$scope.csrf_token = encodeURIComponent(window.__token);
|
||||
$scope.dashboardActive = false;
|
||||
|
||||
$scope.setDashboardActive = function(active) {
|
||||
$scope.dashboardActive = active;
|
||||
};
|
||||
|
||||
$scope.configurationSaved = function() {
|
||||
$scope.requiresRestart = true;
|
||||
};
|
||||
|
||||
$scope.showCreateUser = function() {
|
||||
$scope.createdUser = null;
|
||||
$('#createUserModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.viewSystemLogs = function(service) {
|
||||
if ($scope.pollChannel) {
|
||||
$scope.pollChannel.stop();
|
||||
}
|
||||
|
||||
$scope.debugService = service;
|
||||
$scope.debugLogs = null;
|
||||
|
||||
$scope.pollChannel = AngularPollChannel.create($scope, $scope.loadServiceLogs, 2 * 1000 /* 2s */);
|
||||
$scope.pollChannel.start();
|
||||
};
|
||||
|
||||
$scope.loadServiceLogs = function(callback) {
|
||||
if (!$scope.debugService) { return; }
|
||||
|
||||
var params = {
|
||||
'service': $scope.debugService
|
||||
};
|
||||
|
||||
var errorHandler = ApiService.errorDisplay('Cannot load system logs. Please contact support.',
|
||||
function() {
|
||||
callback(false);
|
||||
})
|
||||
|
||||
ApiService.getSystemLogs(null, params, /* background */true).then(function(resp) {
|
||||
$scope.debugLogs = resp['logs'];
|
||||
callback(true);
|
||||
}, errorHandler);
|
||||
};
|
||||
|
||||
$scope.loadDebugServices = function() {
|
||||
if ($scope.pollChannel) {
|
||||
$scope.pollChannel.stop();
|
||||
}
|
||||
|
||||
$scope.debugService = null;
|
||||
|
||||
ApiService.listSystemLogServices().then(function(resp) {
|
||||
$scope.debugServices = resp['services'];
|
||||
}, ApiService.errorDisplay('Cannot load system logs. Please contact support.'))
|
||||
};
|
||||
|
||||
$scope.getUsage = function() {
|
||||
if ($scope.systemUsage) { return; }
|
||||
|
||||
ApiService.getSystemUsage().then(function(resp) {
|
||||
$scope.systemUsage = resp;
|
||||
}, ApiService.errorDisplay('Cannot load system usage. Please contact support.'))
|
||||
}
|
||||
|
||||
$scope.loadUsageLogs = function() {
|
||||
$scope.logsCounter++;
|
||||
};
|
||||
|
||||
$scope.loadUsers = function() {
|
||||
if ($scope.users) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.loadUsersInternal();
|
||||
};
|
||||
|
||||
$scope.loadUsersInternal = function() {
|
||||
ApiService.listAllUsers().then(function(resp) {
|
||||
$scope.users = resp['users'];
|
||||
$scope.showInterface = true;
|
||||
}, function(resp) {
|
||||
$scope.users = [];
|
||||
$scope.usersError = resp['data']['message'] || resp['data']['error_description'];
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showChangePassword = function(user) {
|
||||
$scope.userToChange = user;
|
||||
$('#changePasswordModal').modal({});
|
||||
};
|
||||
|
||||
$scope.createUser = function() {
|
||||
$scope.creatingUser = true;
|
||||
$scope.createdUser = null;
|
||||
|
||||
var errorHandler = ApiService.errorDisplay('Cannot create user', function() {
|
||||
$scope.creatingUser = false;
|
||||
$('#createUserModal').modal('hide');
|
||||
});
|
||||
|
||||
ApiService.createInstallUser($scope.newUser, null).then(function(resp) {
|
||||
$scope.creatingUser = false;
|
||||
$scope.newUser = {};
|
||||
$scope.createdUser = resp;
|
||||
$scope.loadUsersInternal();
|
||||
}, errorHandler)
|
||||
};
|
||||
|
||||
$scope.showDeleteUser = function(user) {
|
||||
if (user.username == UserService.currentUser().username) {
|
||||
bootbox.dialog({
|
||||
"message": 'Cannot delete yourself!',
|
||||
"title": "Cannot delete user",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.userToDelete = user;
|
||||
$('#confirmDeleteUserModal').modal({});
|
||||
};
|
||||
|
||||
$scope.changeUserPassword = function(user) {
|
||||
$('#changePasswordModal').modal('hide');
|
||||
|
||||
var params = {
|
||||
'username': user.username
|
||||
};
|
||||
|
||||
var data = {
|
||||
'password': user.password
|
||||
};
|
||||
|
||||
ApiService.changeInstallUser(data, params).then(function(resp) {
|
||||
$scope.loadUsersInternal();
|
||||
}, ApiService.errorDisplay('Could not change user'));
|
||||
};
|
||||
|
||||
$scope.deleteUser = function(user) {
|
||||
$('#confirmDeleteUserModal').modal('hide');
|
||||
|
||||
var params = {
|
||||
'username': user.username
|
||||
};
|
||||
|
||||
ApiService.deleteInstallUser(null, params).then(function(resp) {
|
||||
$scope.loadUsersInternal();
|
||||
}, ApiService.errorDisplay('Cannot delete user'));
|
||||
};
|
||||
|
||||
$scope.sendRecoveryEmail = function(user) {
|
||||
var params = {
|
||||
'username': user.username
|
||||
};
|
||||
|
||||
ApiService.sendInstallUserRecoveryEmail(null, params).then(function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": "A recovery email has been sent to " + resp['email'],
|
||||
"title": "Recovery email sent",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}, ApiService.errorDisplay('Cannot send recovery email'))
|
||||
};
|
||||
|
||||
$scope.restartContainer = function() {
|
||||
$('#restartingContainerModal').modal({
|
||||
keyboard: false,
|
||||
backdrop: 'static'
|
||||
});
|
||||
|
||||
ContainerService.restartContainer(function() {
|
||||
$scope.checkStatus()
|
||||
});
|
||||
};
|
||||
|
||||
$scope.checkStatus = function() {
|
||||
ContainerService.checkStatus(function(resp) {
|
||||
$('#restartingContainerModal').modal('hide');
|
||||
$scope.configStatus = resp['status'];
|
||||
$scope.requiresRestart = resp['requires_restart'];
|
||||
|
||||
if ($scope.configStatus == 'ready') {
|
||||
$scope.loadUsers();
|
||||
} else {
|
||||
var message = "Installation of this product has not yet been completed." +
|
||||
"<br><br>Please read the " +
|
||||
"<a href='https://coreos.com/docs/enterprise-registry/initial-setup/'>" +
|
||||
"Setup Guide</a>"
|
||||
|
||||
var title = "Installation Incomplete";
|
||||
CoreDialog.fatal(title, message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Load the initial status.
|
||||
$scope.checkStatus();
|
||||
}
|
765
static/js/core-config-setup.js
Normal file
765
static/js/core-config-setup.js
Normal file
|
@ -0,0 +1,765 @@
|
|||
angular.module("core-config-setup", ['angularFileUpload'])
|
||||
.directive('configSetupTool', function() {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 1,
|
||||
templateUrl: '/static/directives/config/config-setup-tool.html',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'isActive': '=isActive',
|
||||
'configurationSaved': '&configurationSaved'
|
||||
},
|
||||
controller: function($rootScope, $scope, $element, $timeout, ApiService) {
|
||||
$scope.HOSTNAME_REGEX = '^[a-zA-Z-0-9\.]+(:[0-9]+)?$';
|
||||
$scope.GITHUB_REGEX = '^https?://([a-zA-Z0-9]+\.?\/?)+$';
|
||||
|
||||
$scope.SERVICES = [
|
||||
{'id': 'redis', 'title': 'Redis'},
|
||||
|
||||
{'id': 'registry-storage', 'title': 'Registry Storage'},
|
||||
|
||||
{'id': 'ssl', 'title': 'SSL certificate and key', 'condition': function(config) {
|
||||
return config.PREFERRED_URL_SCHEME == 'https';
|
||||
}},
|
||||
|
||||
{'id': 'ldap', 'title': 'LDAP Authentication', 'condition': function(config) {
|
||||
return config.AUTHENTICATION_TYPE == 'LDAP';
|
||||
}},
|
||||
|
||||
{'id': 'mail', 'title': 'E-mail Support', 'condition': function(config) {
|
||||
return config.FEATURE_MAILING;
|
||||
}},
|
||||
|
||||
{'id': 'github-login', 'title': 'Github (Enterprise) Authentication', 'condition': function(config) {
|
||||
return config.FEATURE_GITHUB_LOGIN;
|
||||
}},
|
||||
|
||||
{'id': 'google-login', 'title': 'Google Authentication', 'condition': function(config) {
|
||||
return config.FEATURE_GOOGLE_LOGIN;
|
||||
}},
|
||||
|
||||
{'id': 'github-trigger', 'title': 'Github (Enterprise) Build Triggers', 'condition': function(config) {
|
||||
return config.FEATURE_GITHUB_BUILD;
|
||||
}}
|
||||
];
|
||||
|
||||
$scope.STORAGE_CONFIG_FIELDS = {
|
||||
'LocalStorage': [
|
||||
{'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/some/directory', 'kind': 'text'}
|
||||
],
|
||||
|
||||
'S3Storage': [
|
||||
{'name': 's3_access_key', 'title': 'AWS Access Key', 'placeholder': 'accesskeyhere', 'kind': 'text'},
|
||||
{'name': 's3_secret_key', 'title': 'AWS Secret Key', 'placeholder': 'secretkeyhere', 'kind': 'text'},
|
||||
{'name': 's3_bucket', 'title': 'S3 Bucket', 'placeholder': 'my-cool-bucket', 'kind': 'text'},
|
||||
{'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/path/inside/bucket', 'kind': 'text'}
|
||||
],
|
||||
|
||||
'GoogleCloudStorage': [
|
||||
{'name': 'access_key', 'title': 'Cloud Access Key', 'placeholder': 'accesskeyhere', 'kind': 'text'},
|
||||
{'name': 'secret_key', 'title': 'Cloud Secret Key', 'placeholder': 'secretkeyhere', 'kind': 'text'},
|
||||
{'name': 'bucket_name', 'title': 'GCS Bucket', 'placeholder': 'my-cool-bucket', 'kind': 'text'},
|
||||
{'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/path/inside/bucket', 'kind': 'text'}
|
||||
],
|
||||
|
||||
'RadosGWStorage': [
|
||||
{'name': 'hostname', 'title': 'Rados Server Hostname', 'placeholder': 'my.rados.hostname', 'kind': 'text'},
|
||||
{'name': 'is_secure', 'title': 'Is Secure', 'placeholder': 'Require SSL', 'kind': 'bool'},
|
||||
{'name': 'access_key', 'title': 'Access Key', 'placeholder': 'accesskeyhere', 'kind': 'text', 'help_url': 'http://ceph.com/docs/master/radosgw/admin/'},
|
||||
{'name': 'secret_key', 'title': 'Secret Key', 'placeholder': 'secretkeyhere', 'kind': 'text'},
|
||||
{'name': 'bucket_name', 'title': 'Bucket Name', 'placeholder': 'my-cool-bucket', 'kind': 'text'},
|
||||
{'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/path/inside/bucket', 'kind': 'text'}
|
||||
]
|
||||
};
|
||||
|
||||
$scope.validateHostname = function(hostname) {
|
||||
if (hostname.indexOf('127.0.0.1') == 0 || hostname.indexOf('localhost') == 0) {
|
||||
return 'Please specify a non-localhost hostname. "localhost" will refer to the container, not your machine.'
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
$scope.config = null;
|
||||
$scope.mapped = {
|
||||
'$hasChanges': false
|
||||
};
|
||||
|
||||
$scope.validating = null;
|
||||
$scope.savingConfiguration = false;
|
||||
|
||||
$scope.getServices = function(config) {
|
||||
var services = [];
|
||||
if (!config) { return services; }
|
||||
|
||||
for (var i = 0; i < $scope.SERVICES.length; ++i) {
|
||||
var service = $scope.SERVICES[i];
|
||||
if (!service.condition || service.condition(config)) {
|
||||
services.push({
|
||||
'service': service,
|
||||
'status': 'validating'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return services;
|
||||
};
|
||||
|
||||
$scope.validationStatus = function(serviceInfos) {
|
||||
if (!serviceInfos) { return 'validating'; }
|
||||
|
||||
var hasError = false;
|
||||
for (var i = 0; i < serviceInfos.length; ++i) {
|
||||
if (serviceInfos[i].status == 'validating') {
|
||||
return 'validating';
|
||||
}
|
||||
if (serviceInfos[i].status == 'error') {
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
|
||||
return hasError ? 'failed' : 'success';
|
||||
};
|
||||
|
||||
$scope.cancelValidation = function() {
|
||||
$('#validateAndSaveModal').modal('hide');
|
||||
$scope.validating = null;
|
||||
$scope.savingConfiguration = false;
|
||||
};
|
||||
|
||||
$scope.validateService = function(serviceInfo) {
|
||||
var params = {
|
||||
'service': serviceInfo.service.id
|
||||
};
|
||||
|
||||
ApiService.scValidateConfig({'config': $scope.config}, params).then(function(resp) {
|
||||
serviceInfo.status = resp.status ? 'success' : 'error';
|
||||
serviceInfo.errorMessage = $.trim(resp.reason || '');
|
||||
}, ApiService.errorDisplay('Could not validate configuration. Please report this error.'));
|
||||
};
|
||||
|
||||
$scope.checkValidateAndSave = function() {
|
||||
if ($scope.configform.$valid) {
|
||||
$scope.validateAndSave();
|
||||
return;
|
||||
}
|
||||
|
||||
$element.find("input.ng-invalid:first")[0].scrollIntoView();
|
||||
$element.find("input.ng-invalid:first").focus();
|
||||
};
|
||||
|
||||
$scope.validateAndSave = function() {
|
||||
$scope.savingConfiguration = false;
|
||||
$scope.validating = $scope.getServices($scope.config);
|
||||
|
||||
$('#validateAndSaveModal').modal({
|
||||
keyboard: false,
|
||||
backdrop: 'static'
|
||||
});
|
||||
|
||||
for (var i = 0; i < $scope.validating.length; ++i) {
|
||||
var serviceInfo = $scope.validating[i];
|
||||
$scope.validateService(serviceInfo);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.saveConfiguration = function() {
|
||||
$scope.savingConfiguration = true;
|
||||
|
||||
// Make sure to note that fully verified setup is completed. We use this as a signal
|
||||
// in the setup tool.
|
||||
$scope.config['SETUP_COMPLETE'] = true;
|
||||
|
||||
var data = {
|
||||
'config': $scope.config,
|
||||
'hostname': window.location.host
|
||||
};
|
||||
|
||||
ApiService.scUpdateConfig(data).then(function(resp) {
|
||||
$scope.savingConfiguration = false;
|
||||
$scope.mapped.$hasChanges = false;
|
||||
$('#validateAndSaveModal').modal('hide');
|
||||
$scope.configurationSaved({'config': $scope.config});
|
||||
}, ApiService.errorDisplay('Could not save configuration. Please report this error.'));
|
||||
};
|
||||
|
||||
var githubSelector = function(key) {
|
||||
return function(value) {
|
||||
if (!value || !$scope.config) { return; }
|
||||
|
||||
if (!$scope.config[key]) {
|
||||
$scope.config[key] = {};
|
||||
}
|
||||
|
||||
if (value == 'enterprise') {
|
||||
if ($scope.config[key]['GITHUB_ENDPOINT'] == 'https://github.com/') {
|
||||
$scope.config[key]['GITHUB_ENDPOINT'] = '';
|
||||
}
|
||||
delete $scope.config[key]['API_ENDPOINT'];
|
||||
} else if (value == 'hosted') {
|
||||
$scope.config[key]['GITHUB_ENDPOINT'] = 'https://github.com/';
|
||||
$scope.config[key]['API_ENDPOINT'] = 'https://api.github.com/';
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
var getKey = function(config, path) {
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var parts = path.split('.');
|
||||
var current = config;
|
||||
for (var i = 0; i < parts.length; ++i) {
|
||||
var part = parts[i];
|
||||
if (!current[part]) { return null; }
|
||||
current = current[part];
|
||||
}
|
||||
return current;
|
||||
};
|
||||
|
||||
var initializeMappedLogic = function(config) {
|
||||
var gle = getKey(config, 'GITHUB_LOGIN_CONFIG.GITHUB_ENDPOINT');
|
||||
var gte = getKey(config, 'GITHUB_TRIGGER_CONFIG.GITHUB_ENDPOINT');
|
||||
|
||||
$scope.mapped['GITHUB_LOGIN_KIND'] = gle == 'https://github.com/' ? 'hosted' : 'enterprise';
|
||||
$scope.mapped['GITHUB_TRIGGER_KIND'] = gte == 'https://github.com/' ? 'hosted' : 'enterprise';
|
||||
|
||||
$scope.mapped['redis'] = {};
|
||||
$scope.mapped['redis']['host'] = getKey(config, 'BUILDLOGS_REDIS.host') || getKey(config, 'USER_EVENTS_REDIS.host');
|
||||
$scope.mapped['redis']['port'] = getKey(config, 'BUILDLOGS_REDIS.port') || getKey(config, 'USER_EVENTS_REDIS.port');
|
||||
$scope.mapped['redis']['password'] = getKey(config, 'BUILDLOGS_REDIS.password') || getKey(config, 'USER_EVENTS_REDIS.password');
|
||||
};
|
||||
|
||||
var redisSetter = function(keyname) {
|
||||
return function(value) {
|
||||
if (value == null || !$scope.config) { return; }
|
||||
|
||||
if (!$scope.config['BUILDLOGS_REDIS']) {
|
||||
$scope.config['BUILDLOGS_REDIS'] = {};
|
||||
}
|
||||
|
||||
if (!$scope.config['USER_EVENTS_REDIS']) {
|
||||
$scope.config['USER_EVENTS_REDIS'] = {};
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
delete $scope.config['BUILDLOGS_REDIS'][keyname];
|
||||
delete $scope.config['USER_EVENTS_REDIS'][keyname];
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.config['BUILDLOGS_REDIS'][keyname] = value;
|
||||
$scope.config['USER_EVENTS_REDIS'][keyname] = value;
|
||||
};
|
||||
};
|
||||
|
||||
// Add mapped logic.
|
||||
$scope.$watch('mapped.GITHUB_LOGIN_KIND', githubSelector('GITHUB_LOGIN_CONFIG'));
|
||||
$scope.$watch('mapped.GITHUB_TRIGGER_KIND', githubSelector('GITHUB_TRIGGER_CONFIG'));
|
||||
|
||||
$scope.$watch('mapped.redis.host', redisSetter('host'));
|
||||
$scope.$watch('mapped.redis.port', redisSetter('port'));
|
||||
$scope.$watch('mapped.redis.password', redisSetter('password'));
|
||||
|
||||
// Add a watch to remove any fields not allowed by the current storage configuration.
|
||||
// We have to do this otherwise extra fields (which are not allowed) can end up in the
|
||||
// configuration.
|
||||
$scope.$watch('config.DISTRIBUTED_STORAGE_CONFIG.local[0]', function(value) {
|
||||
// Remove any fields not associated with the current kind.
|
||||
if (!value || !$scope.STORAGE_CONFIG_FIELDS[value]
|
||||
|| !$scope.config.DISTRIBUTED_STORAGE_CONFIG
|
||||
|| !$scope.config.DISTRIBUTED_STORAGE_CONFIG.local
|
||||
|| !$scope.config.DISTRIBUTED_STORAGE_CONFIG.local[1]) { return; }
|
||||
|
||||
var allowedFields = $scope.STORAGE_CONFIG_FIELDS[value];
|
||||
var configObject = $scope.config.DISTRIBUTED_STORAGE_CONFIG.local[1];
|
||||
|
||||
// Remove any fields not allowed.
|
||||
for (var fieldName in configObject) {
|
||||
if (!configObject.hasOwnProperty(fieldName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var isValidField = $.grep(allowedFields, function(field) {
|
||||
return field.name == fieldName;
|
||||
}).length > 0;
|
||||
|
||||
if (!isValidField) {
|
||||
delete configObject[fieldName];
|
||||
}
|
||||
}
|
||||
|
||||
// Set any boolean fields to false.
|
||||
for (var i = 0; i < allowedFields.length; ++i) {
|
||||
if (allowedFields[i].kind == 'bool') {
|
||||
configObject[allowedFields[i].name] = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('config', function(value) {
|
||||
$scope.mapped['$hasChanges'] = true;
|
||||
}, true);
|
||||
|
||||
$scope.$watch('isActive', function(value) {
|
||||
if (!value) { return; }
|
||||
|
||||
ApiService.scGetConfig().then(function(resp) {
|
||||
$scope.config = resp['config'] || {};
|
||||
initializeMappedLogic($scope.config);
|
||||
$scope.mapped['$hasChanges'] = false;
|
||||
}, ApiService.errorDisplay('Could not load config'));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('configParsedField', function ($timeout) {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/config/config-parsed-field.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'binding': '=binding',
|
||||
'parser': '&parser',
|
||||
'serializer': '&serializer'
|
||||
},
|
||||
controller: function($scope, $element, $transclude) {
|
||||
$scope.childScope = null;
|
||||
|
||||
$transclude(function(clone, scope) {
|
||||
$scope.childScope = scope;
|
||||
$scope.childScope['fields'] = {};
|
||||
$element.append(clone);
|
||||
});
|
||||
|
||||
$scope.childScope.$watch('fields', function(value) {
|
||||
// Note: We need the timeout here because Angular starts the digest of the
|
||||
// parent scope AFTER the child scope, which means it can end up one action
|
||||
// behind. The timeout ensures that the parent scope will be fully digest-ed
|
||||
// and then we update the binding. Yes, this is a hack :-/.
|
||||
$timeout(function() {
|
||||
$scope.binding = $scope.serializer({'fields': value});
|
||||
});
|
||||
}, true);
|
||||
|
||||
$scope.$watch('binding', function(value) {
|
||||
var parsed = $scope.parser({'value': value});
|
||||
for (var key in parsed) {
|
||||
if (parsed.hasOwnProperty(key)) {
|
||||
$scope.childScope['fields'][key] = parsed[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('configVariableField', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/config/config-variable-field.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'binding': '=binding'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.sections = {};
|
||||
$scope.currentSection = null;
|
||||
|
||||
$scope.setSection = function(section) {
|
||||
$scope.binding = section.value;
|
||||
};
|
||||
|
||||
this.addSection = function(section, element) {
|
||||
$scope.sections[section.value] = {
|
||||
'title': section.valueTitle,
|
||||
'value': section.value,
|
||||
'element': element
|
||||
};
|
||||
|
||||
element.hide();
|
||||
|
||||
if (!$scope.binding) {
|
||||
$scope.binding = section.value;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('binding', function(binding) {
|
||||
if (!binding) { return; }
|
||||
|
||||
if ($scope.currentSection) {
|
||||
$scope.currentSection.element.hide();
|
||||
}
|
||||
|
||||
if ($scope.sections[binding]) {
|
||||
$scope.sections[binding].element.show();
|
||||
$scope.currentSection = $scope.sections[binding];
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('variableSection', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/config/config-variable-field.html',
|
||||
priority: 1,
|
||||
require: '^configVariableField',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'value': '@value',
|
||||
'valueTitle': '@valueTitle'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
var parentCtrl = $element.parent().controller('configVariableField');
|
||||
parentCtrl.addSection($scope, $element);
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('configListField', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/config/config-list-field.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'binding': '=binding',
|
||||
'placeholder': '@placeholder',
|
||||
'defaultValue': '@defaultValue',
|
||||
'itemTitle': '@itemTitle'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.removeItem = function(item) {
|
||||
var index = $scope.binding.indexOf(item);
|
||||
if (index >= 0) {
|
||||
$scope.binding.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.addItem = function() {
|
||||
if (!$scope.newItemName) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$scope.binding) {
|
||||
$scope.binding = [];
|
||||
}
|
||||
|
||||
if ($scope.binding.indexOf($scope.newItemName) >= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.binding.push($scope.newItemName);
|
||||
$scope.newItemName = null;
|
||||
};
|
||||
|
||||
$scope.$watch('binding', function(binding) {
|
||||
if (!binding && $scope.defaultValue) {
|
||||
$scope.binding = eval($scope.defaultValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('configFileField', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/config/config-file-field.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'filename': '@filename'
|
||||
},
|
||||
controller: function($scope, $element, Restangular, $upload) {
|
||||
$scope.hasFile = false;
|
||||
|
||||
$scope.onFileSelect = function(files) {
|
||||
if (files.length < 1) { return; }
|
||||
|
||||
$scope.uploadProgress = 0;
|
||||
$scope.upload = $upload.upload({
|
||||
url: '/api/v1/superuser/config/file/' + $scope.filename,
|
||||
method: 'POST',
|
||||
data: {'_csrf_token': window.__token},
|
||||
file: files[0],
|
||||
}).progress(function(evt) {
|
||||
$scope.uploadProgress = parseInt(100.0 * evt.loaded / evt.total);
|
||||
if ($scope.uploadProgress == 100) {
|
||||
$scope.uploadProgress = null;
|
||||
$scope.hasFile = true;
|
||||
}
|
||||
}).success(function(data, status, headers, config) {
|
||||
$scope.uploadProgress = null;
|
||||
$scope.hasFile = true;
|
||||
});
|
||||
};
|
||||
|
||||
var loadStatus = function(filename) {
|
||||
Restangular.one('superuser/config/file/' + filename).get().then(function(resp) {
|
||||
$scope.hasFile = resp['exists'];
|
||||
});
|
||||
};
|
||||
|
||||
if ($scope.filename) {
|
||||
loadStatus($scope.filename);
|
||||
}
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('configBoolField', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/config/config-bool-field.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'binding': '=binding'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('configNumericField', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/config/config-numeric-field.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'binding': '=binding',
|
||||
'placeholder': '@placeholder',
|
||||
'defaultValue': '@defaultValue'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.bindinginternal = 0;
|
||||
|
||||
$scope.$watch('binding', function(binding) {
|
||||
if ($scope.binding == 0 && $scope.defaultValue) {
|
||||
$scope.binding = $scope.defaultValue * 1;
|
||||
}
|
||||
|
||||
$scope.bindinginternal = $scope.binding;
|
||||
});
|
||||
|
||||
$scope.$watch('bindinginternal', function(binding) {
|
||||
var newValue = $scope.bindinginternal * 1;
|
||||
if (isNaN(newValue)) {
|
||||
newValue = 0;
|
||||
}
|
||||
$scope.binding = newValue;
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('configContactsField', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/config/config-contacts-field.html',
|
||||
priority: 1,
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'binding': '=binding'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
var padItems = function(items) {
|
||||
// Remove the last item if both it and the second to last items are empty.
|
||||
if (items.length > 1 && !items[items.length - 2].value && !items[items.length - 1].value) {
|
||||
items.splice(items.length - 1, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the last item is non-empty, add a new item.
|
||||
if (items.length == 0 || items[items.length - 1].value) {
|
||||
items.push({'value': ''});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.itemHash = null;
|
||||
$scope.$watch('items', function(items) {
|
||||
if (!items) { return; }
|
||||
padItems(items);
|
||||
|
||||
var itemHash = '';
|
||||
var binding = [];
|
||||
for (var i = 0; i < items.length; ++i) {
|
||||
var item = items[i];
|
||||
if (item.value && (URI(item.value).host() || URI(item.value).path())) {
|
||||
binding.push(item.value);
|
||||
itemHash += item.value;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.itemHash = itemHash;
|
||||
$scope.binding = binding;
|
||||
}, true);
|
||||
|
||||
$scope.$watch('binding', function(binding) {
|
||||
var current = binding || [];
|
||||
var items = [];
|
||||
var itemHash = '';
|
||||
for (var i = 0; i < current.length; ++i) {
|
||||
items.push({'value': current[i]})
|
||||
itemHash += current[i];
|
||||
}
|
||||
|
||||
if ($scope.itemHash != itemHash) {
|
||||
$scope.items = items;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('configContactField', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/config/config-contact-field.html',
|
||||
priority: 1,
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'binding': '=binding'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.kind = null;
|
||||
$scope.value = null;
|
||||
|
||||
var updateBinding = function() {
|
||||
if ($scope.value == null) { return; }
|
||||
var value = $scope.value || '';
|
||||
|
||||
switch ($scope.kind) {
|
||||
case 'mailto':
|
||||
$scope.binding = 'mailto:' + value;
|
||||
return;
|
||||
|
||||
case 'tel':
|
||||
$scope.binding = 'tel:' + value;
|
||||
return;
|
||||
|
||||
case 'irc':
|
||||
$scope.binding = 'irc://' + value;
|
||||
return;
|
||||
|
||||
default:
|
||||
$scope.binding = value;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('kind', updateBinding);
|
||||
$scope.$watch('value', updateBinding);
|
||||
|
||||
$scope.$watch('binding', function(value) {
|
||||
if (!value) {
|
||||
$scope.kind = null;
|
||||
$scope.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var uri = URI(value);
|
||||
$scope.kind = uri.scheme();
|
||||
|
||||
switch ($scope.kind) {
|
||||
case 'mailto':
|
||||
case 'tel':
|
||||
$scope.value = uri.path();
|
||||
break;
|
||||
|
||||
case 'irc':
|
||||
$scope.value = value.substr('irc://'.length);
|
||||
break;
|
||||
|
||||
default:
|
||||
$scope.kind = 'http';
|
||||
$scope.value = value;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
$scope.getPlaceholder = function(kind) {
|
||||
switch (kind) {
|
||||
case 'mailto':
|
||||
return 'some@example.com';
|
||||
|
||||
case 'tel':
|
||||
return '555-555-5555';
|
||||
|
||||
case 'irc':
|
||||
return 'myserver:port/somechannel';
|
||||
|
||||
default:
|
||||
return 'http://some/url';
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('configStringField', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/config/config-string-field.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'binding': '=binding',
|
||||
'placeholder': '@placeholder',
|
||||
'pattern': '@pattern',
|
||||
'defaultValue': '@defaultValue',
|
||||
'validator': '&validator'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.getRegexp = function(pattern) {
|
||||
if (!pattern) {
|
||||
pattern = '.*';
|
||||
}
|
||||
return new RegExp(pattern);
|
||||
};
|
||||
|
||||
$scope.$watch('binding', function(binding) {
|
||||
if (!binding && $scope.defaultValue) {
|
||||
$scope.binding = $scope.defaultValue;
|
||||
}
|
||||
|
||||
$scope.errorMessage = $scope.validator({'value': binding || ''});
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
568
static/js/core-ui.js
Normal file
568
static/js/core-ui.js
Normal file
|
@ -0,0 +1,568 @@
|
|||
angular.module("core-ui", [])
|
||||
.factory('CoreDialog', [function() {
|
||||
var service = {};
|
||||
service['fatal'] = function(title, message) {
|
||||
bootbox.dialog({
|
||||
"title": title,
|
||||
"message": "<div class='alert-icon-container-container'><div class='alert-icon-container'><div class='alert-icon'></div></div></div>" + message,
|
||||
"buttons": {},
|
||||
"className": "co-dialog fatal-error",
|
||||
"closeButton": false
|
||||
});
|
||||
};
|
||||
|
||||
return service;
|
||||
}])
|
||||
|
||||
.directive('corLogBox', function() {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 1,
|
||||
templateUrl: '/static/directives/cor-log-box.html',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'logs': '=logs'
|
||||
},
|
||||
controller: function($rootScope, $scope, $element, $timeout) {
|
||||
$scope.hasNewLogs = false;
|
||||
|
||||
var scrollHandlerBound = false;
|
||||
var isAnimatedScrolling = false;
|
||||
var isScrollBottom = true;
|
||||
|
||||
var scrollHandler = function() {
|
||||
if (isAnimatedScrolling) { return; }
|
||||
var element = $element.find("#co-log-viewer")[0];
|
||||
isScrollBottom = element.scrollHeight - element.scrollTop === element.clientHeight;
|
||||
if (isScrollBottom) {
|
||||
$scope.hasNewLogs = false;
|
||||
}
|
||||
};
|
||||
|
||||
var animateComplete = function() {
|
||||
isAnimatedScrolling = false;
|
||||
};
|
||||
|
||||
$scope.moveToBottom = function() {
|
||||
$scope.hasNewLogs = false;
|
||||
isAnimatedScrolling = true;
|
||||
isScrollBottom = true;
|
||||
|
||||
$element.find("#co-log-viewer").animate(
|
||||
{ scrollTop: $element.find("#co-log-content").height() }, "slow", null, animateComplete);
|
||||
};
|
||||
|
||||
$scope.$watch('logs', function(value, oldValue) {
|
||||
if (!value) { return; }
|
||||
|
||||
$timeout(function() {
|
||||
if (!scrollHandlerBound) {
|
||||
$element.find("#co-log-viewer").on('scroll', scrollHandler);
|
||||
scrollHandlerBound = true;
|
||||
}
|
||||
|
||||
if (!isScrollBottom) {
|
||||
$scope.hasNewLogs = true;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.moveToBottom();
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('corOptionsMenu', function() {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 1,
|
||||
templateUrl: '/static/directives/cor-options-menu.html',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {},
|
||||
controller: function($rootScope, $scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('corOption', function() {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 1,
|
||||
templateUrl: '/static/directives/cor-option.html',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'optionClick': '&optionClick'
|
||||
},
|
||||
controller: function($rootScope, $scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
|
||||
.directive('corTitle', function() {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 1,
|
||||
templateUrl: '/static/directives/cor-title.html',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {},
|
||||
controller: function($rootScope, $scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('corTitleContent', function() {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 1,
|
||||
templateUrl: '/static/directives/cor-title-content.html',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {},
|
||||
controller: function($rootScope, $scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('corTitleLink', function() {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 1,
|
||||
templateUrl: '/static/directives/cor-title-link.html',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {},
|
||||
controller: function($rootScope, $scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('corTabPanel', function() {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 1,
|
||||
templateUrl: '/static/directives/cor-tab-panel.html',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {},
|
||||
controller: function($rootScope, $scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('corTabContent', function() {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 2,
|
||||
templateUrl: '/static/directives/cor-tab-content.html',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {},
|
||||
controller: function($rootScope, $scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('corTabs', function() {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 3,
|
||||
templateUrl: '/static/directives/cor-tabs.html',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {},
|
||||
controller: function($rootScope, $scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('corFloatingBottomBar', function() {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 3,
|
||||
templateUrl: '/static/directives/cor-floating-bottom-bar.html',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {},
|
||||
controller: function($rootScope, $scope, $element, $timeout, $interval) {
|
||||
var handler = function() {
|
||||
$element.removeClass('floating');
|
||||
$element.css('width', $element[0].parentNode.clientWidth + 'px');
|
||||
|
||||
var windowHeight = $(window).height();
|
||||
var rect = $element[0].getBoundingClientRect();
|
||||
if (rect.bottom > windowHeight) {
|
||||
$element.addClass('floating');
|
||||
}
|
||||
};
|
||||
|
||||
$(window).on("scroll", handler);
|
||||
$(window).on("resize", handler);
|
||||
|
||||
var previousHeight = $element[0].parentNode.clientHeight;
|
||||
var stop = $interval(function() {
|
||||
var currentHeight = $element[0].parentNode.clientWidth;
|
||||
if (previousHeight != currentHeight) {
|
||||
currentHeight = previousHeight;
|
||||
handler();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
$scope.$on('$destroy', function() {
|
||||
$(window).off("resize", handler);
|
||||
$(window).off("scroll", handler);
|
||||
$interval.cancel(stop);
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
|
||||
})
|
||||
|
||||
.directive('corLoaderInline', function() {
|
||||
var directiveDefinitionObject = {
|
||||
templateUrl: '/static/directives/cor-loader-inline.html',
|
||||
replace: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
},
|
||||
controller: function($rootScope, $scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('corLoader', function() {
|
||||
var directiveDefinitionObject = {
|
||||
templateUrl: '/static/directives/cor-loader.html',
|
||||
replace: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
},
|
||||
controller: function($rootScope, $scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('corTab', function() {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 4,
|
||||
templateUrl: '/static/directives/cor-tab.html',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'tabActive': '@tabActive',
|
||||
'tabTitle': '@tabTitle',
|
||||
'tabTarget': '@tabTarget',
|
||||
'tabInit': '&tabInit',
|
||||
'tabShown': '&tabShown',
|
||||
'tabHidden': '&tabHidden'
|
||||
},
|
||||
controller: function($rootScope, $scope, $element) {
|
||||
$element.find('a[data-toggle="tab"]').on('hidden.bs.tab', function (e) {
|
||||
$scope.tabHidden({});
|
||||
});
|
||||
|
||||
$element.find('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||
$scope.tabShown({});
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('corStep', function() {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 4,
|
||||
templateUrl: '/static/directives/cor-step.html',
|
||||
replace: true,
|
||||
transclude: false,
|
||||
requires: '^corStepBar',
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'icon': '@icon',
|
||||
'title': '@title',
|
||||
'text': '@text'
|
||||
},
|
||||
controller: function($rootScope, $scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('realtimeAreaChart', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/realtime-area-chart.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'data': '=data',
|
||||
'labels': '=labels',
|
||||
'colors': '=colors',
|
||||
'counter': '=counter'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
var graph = null;
|
||||
var series = [];
|
||||
var palette = new Rickshaw.Color.Palette( { scheme: 'spectrum14' } );
|
||||
var colors = $scope.colors || [];
|
||||
|
||||
var setupGraph = function() {
|
||||
for (var i = 0; i < $scope.labels.length; ++i) {
|
||||
series.push({
|
||||
name: $scope.labels[i],
|
||||
color: i >= colors.length ? palette.color(): $scope.colors[i],
|
||||
stroke: 'rgba(0,0,0,0.15)',
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
var options = {
|
||||
element: $element.find('.chart')[0],
|
||||
renderer: 'area',
|
||||
stroke: true,
|
||||
series: series,
|
||||
min: 0,
|
||||
padding: {
|
||||
'top': 0.3,
|
||||
'left': 0,
|
||||
'right': 0,
|
||||
'bottom': 0.3
|
||||
}
|
||||
};
|
||||
|
||||
if ($scope.minimum != null) {
|
||||
options['min'] = $scope.minimum == 'auto' ? 'auto' : $scope.minimum * 1;
|
||||
} else {
|
||||
options['min'] = 0;
|
||||
}
|
||||
|
||||
if ($scope.maximum != null) {
|
||||
options['max'] = $scope.maximum == 'auto' ? 'auto' : $scope.maximum * 1;
|
||||
}
|
||||
|
||||
graph = new Rickshaw.Graph(options);
|
||||
|
||||
xaxes = new Rickshaw.Graph.Axis.Time({
|
||||
graph: graph,
|
||||
timeFixture: new Rickshaw.Fixtures.Time.Local()
|
||||
});
|
||||
|
||||
yaxes = new Rickshaw.Graph.Axis.Y({
|
||||
graph: graph,
|
||||
tickFormat: Rickshaw.Fixtures.Number.formatKMBT
|
||||
});
|
||||
|
||||
hoverDetail = new Rickshaw.Graph.HoverDetail({
|
||||
graph: graph,
|
||||
xFormatter: function(x) {
|
||||
return new Date(x * 1000).toString();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var refresh = function(data) {
|
||||
if (!data || $scope.counter < 0) { return; }
|
||||
if (!graph) {
|
||||
setupGraph();
|
||||
}
|
||||
|
||||
var timecode = new Date().getTime() / 1000;
|
||||
for (var i = 0; i < $scope.data.length; ++i) {
|
||||
var arr = series[i].data;
|
||||
arr.push(
|
||||
{'x': timecode, 'y': $scope.data[i] }
|
||||
);
|
||||
|
||||
if (arr.length > 10) {
|
||||
series[i].data = arr.slice(arr.length - 10, arr.length);
|
||||
}
|
||||
}
|
||||
|
||||
graph.renderer.unstack = true;
|
||||
graph.update();
|
||||
};
|
||||
|
||||
$scope.$watch('counter', function() {
|
||||
refresh($scope.data_raw);
|
||||
});
|
||||
|
||||
$scope.$watch('data', function(data) {
|
||||
$scope.data_raw = data;
|
||||
refresh($scope.data_raw);
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
|
||||
.directive('realtimeLineChart', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/realtime-line-chart.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'data': '=data',
|
||||
'labels': '=labels',
|
||||
'counter': '=counter',
|
||||
'labelTemplate': '@labelTemplate',
|
||||
'minimum': '@minimum',
|
||||
'maximum': '@maximum'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
var graph = null;
|
||||
var xaxes = null;
|
||||
var yaxes = null;
|
||||
var hoverDetail = null;
|
||||
var series = [];
|
||||
var counter = 0;
|
||||
var palette = new Rickshaw.Color.Palette( { scheme: 'spectrum14' } );
|
||||
|
||||
var setupGraph = function() {
|
||||
var options = {
|
||||
element: $element.find('.chart')[0],
|
||||
renderer: 'line',
|
||||
series: series,
|
||||
padding: {
|
||||
'top': 0.3,
|
||||
'left': 0,
|
||||
'right': 0,
|
||||
'bottom': 0.3
|
||||
}
|
||||
};
|
||||
|
||||
if ($scope.minimum != null) {
|
||||
options['min'] = $scope.minimum == 'auto' ? 'auto' : $scope.minimum * 1;
|
||||
} else {
|
||||
options['min'] = 0;
|
||||
}
|
||||
|
||||
if ($scope.maximum != null) {
|
||||
options['max'] = $scope.maximum == 'auto' ? 'auto' : $scope.maximum * 1;
|
||||
}
|
||||
|
||||
graph = new Rickshaw.Graph(options);
|
||||
xaxes = new Rickshaw.Graph.Axis.Time({
|
||||
graph: graph,
|
||||
timeFixture: new Rickshaw.Fixtures.Time.Local()
|
||||
});
|
||||
|
||||
yaxes = new Rickshaw.Graph.Axis.Y({
|
||||
graph: graph,
|
||||
tickFormat: Rickshaw.Fixtures.Number.formatKMBT
|
||||
});
|
||||
|
||||
hoverDetail = new Rickshaw.Graph.HoverDetail({
|
||||
graph: graph,
|
||||
xFormatter: function(x) {
|
||||
return new Date(x * 1000).toString();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var refresh = function(data) {
|
||||
if (data == null) { return; }
|
||||
if (!graph) {
|
||||
setupGraph();
|
||||
}
|
||||
|
||||
if (typeof data == 'number') {
|
||||
data = [data];
|
||||
}
|
||||
|
||||
if ($scope.labels) {
|
||||
data = data.slice(0, $scope.labels.length);
|
||||
}
|
||||
|
||||
if (series.length == 0){
|
||||
for (var i = 0; i < data.length; ++i) {
|
||||
var title = $scope.labels ? $scope.labels[i] : $scope.labelTemplate.replace('{x}', i + 1);
|
||||
series.push({
|
||||
'color': palette.color(),
|
||||
'data': [],
|
||||
'name': title
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
counter++;
|
||||
var timecode = new Date().getTime() / 1000;
|
||||
|
||||
for (var i = 0; i < data.length; ++i) {
|
||||
var arr = series[i].data;
|
||||
arr.push({
|
||||
'x': timecode,
|
||||
'y': data[i]
|
||||
})
|
||||
|
||||
if (arr.length > 10) {
|
||||
series[i].data = arr.slice(arr.length - 10, arr.length);
|
||||
}
|
||||
}
|
||||
|
||||
graph.update();
|
||||
};
|
||||
|
||||
$scope.$watch('counter', function(counter) {
|
||||
refresh($scope.data_raw);
|
||||
});
|
||||
|
||||
$scope.$watch('data', function(data) {
|
||||
$scope.data_raw = data;
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('corStepBar', function() {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 4,
|
||||
templateUrl: '/static/directives/cor-step-bar.html',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'progress': '=progress'
|
||||
},
|
||||
controller: function($rootScope, $scope, $element) {
|
||||
$scope.$watch('progress', function(progress) {
|
||||
var index = 0;
|
||||
for (var i = 0; i < progress.length; ++i) {
|
||||
if (progress[i]) {
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
|
||||
$element.find('.transclude').children('.co-step-element').each(function(i, elem) {
|
||||
$(elem).removeClass('active');
|
||||
if (i <= index) {
|
||||
$(elem).addClass('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -485,6 +485,9 @@ ImageHistoryTree.prototype.buildRoot_ = function() {
|
|||
maxChildHeight = Math.max(maxChildHeight, key);
|
||||
});
|
||||
|
||||
// Recursively prune the nodes that are not referenced by a tag
|
||||
this.pruneUnreferenced_(root);
|
||||
|
||||
// Compact the graph so that any single chain of three (or more) images becomes a collapsed
|
||||
// section. We only do this if the max width is > 1 (since for a single width tree, no long
|
||||
// chain will hide a branch).
|
||||
|
@ -505,6 +508,23 @@ ImageHistoryTree.prototype.buildRoot_ = function() {
|
|||
};
|
||||
|
||||
|
||||
/**
|
||||
* Prunes images which are not referenced either directly or indirectly by any tag.
|
||||
*/
|
||||
ImageHistoryTree.prototype.pruneUnreferenced_ = function(node) {
|
||||
if (node.children) {
|
||||
var surviving_children = []
|
||||
for (var i = 0; i < node.children.length; ++i) {
|
||||
if (!this.pruneUnreferenced_(node.children[i])) {
|
||||
surviving_children.push(node.children[i]);
|
||||
}
|
||||
}
|
||||
node.children = surviving_children;
|
||||
}
|
||||
return (node.children.length == 0 && node.tags.length == 0);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Determines the height of the tree at its longest chain.
|
||||
*/
|
||||
|
|
Reference in a new issue