Merge branch 'newbuildview'
This commit is contained in:
commit
43ab838998
33 changed files with 1095 additions and 36 deletions
|
@ -96,6 +96,9 @@ quayApp.config(['$routeProvider', '$locationProvider', 'pages', function($routeP
|
|||
// Repo Builds
|
||||
.route('/repository/:namespace/:name/build', 'repo-build')
|
||||
|
||||
// Repo Build View
|
||||
.route('/repository/:namespace/:name/build/:buildid', 'build-view')
|
||||
|
||||
// Repo Build Package
|
||||
.route('/repository/:namespace/:name/build/:buildid/buildpack', 'build-package')
|
||||
|
||||
|
@ -203,8 +206,8 @@ if (window.__config && window.__config.SENTRY_PUBLIC_DSN) {
|
|||
}
|
||||
|
||||
// Run the application.
|
||||
quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanService', '$http', '$timeout', 'CookieService', 'Features', '$anchorScroll', 'UtilService',
|
||||
function($location, $rootScope, Restangular, UserService, PlanService, $http, $timeout, CookieService, Features, $anchorScroll, UtilService) {
|
||||
quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanService', '$http', '$timeout', 'CookieService', 'Features', '$anchorScroll', 'UtilService', 'MetaService',
|
||||
function($location, $rootScope, Restangular, UserService, PlanService, $http, $timeout, CookieService, Features, $anchorScroll, UtilService, MetaService) {
|
||||
|
||||
var title = window.__config['REGISTRY_TITLE'] || 'Quay.io';
|
||||
|
||||
|
@ -296,34 +299,39 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
|
|||
});
|
||||
|
||||
$rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
|
||||
$rootScope.pageClass = '';
|
||||
$rootScope.current = current.$$route;
|
||||
$rootScope.currentPage = current;
|
||||
|
||||
$rootScope.pageClass = '';
|
||||
|
||||
if (!current.$$route) { return; }
|
||||
|
||||
if (current.$$route.title) {
|
||||
$rootScope.title = current.$$route.title;
|
||||
} else {
|
||||
$rootScope.title = title;
|
||||
}
|
||||
|
||||
if (current.$$route.pageClass) {
|
||||
$rootScope.pageClass = current.$$route.pageClass;
|
||||
}
|
||||
|
||||
$rootScope.pageClass = current.$$route.pageClass || '';
|
||||
$rootScope.newLayout = !!current.$$route.newLayout;
|
||||
|
||||
if (current.$$route.description) {
|
||||
$rootScope.description = current.$$route.description;
|
||||
} else {
|
||||
$rootScope.description = '';
|
||||
}
|
||||
|
||||
$rootScope.fixFooter = !!current.$$route.fixFooter;
|
||||
|
||||
MetaService.getInitialTitle(current, function(title) {
|
||||
$rootScope.title = title;
|
||||
});
|
||||
|
||||
MetaService.getInitialDescription(current, function(description) {
|
||||
$rootScope.description = description
|
||||
});
|
||||
|
||||
$anchorScroll();
|
||||
});
|
||||
|
||||
$rootScope.$on('$viewContentLoaded', function(event, current) {
|
||||
$rootScope.$on('$viewContentLoaded', function(event) {
|
||||
var current = $rootScope.currentPage;
|
||||
|
||||
MetaService.getTitle(current, function(title) {
|
||||
$rootScope.title = title;
|
||||
});
|
||||
|
||||
MetaService.getDescription(current, function(description) {
|
||||
$rootScope.description = description;
|
||||
});
|
||||
|
||||
var activeTab = $location.search()['tab'];
|
||||
|
||||
// Setup deep linking of tabs. This will change the search field of the URL whenever a tab
|
||||
|
|
19
static/js/directives/ui/build-info-bar.js
Normal file
19
static/js/directives/ui/build-info-bar.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* An element which displays the status of a build in a nice compact bar.
|
||||
*/
|
||||
angular.module('quay').directive('buildInfoBar', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-info-bar.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'build': '=build',
|
||||
'showTime': '=showTime'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
168
static/js/directives/ui/build-logs-view.js
Normal file
168
static/js/directives/ui/build-logs-view.js
Normal file
|
@ -0,0 +1,168 @@
|
|||
/**
|
||||
* An element which displays and auto-updates the logs from a build.
|
||||
*/
|
||||
angular.module('quay').directive('buildLogsView', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-logs-view.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'build': '=build',
|
||||
'useTimestamps': '=useTimestamps',
|
||||
'buildUpdated': '&buildUpdated'
|
||||
},
|
||||
controller: function($scope, $element, $interval, $sanitize, ansi2html, AngularViewArray,
|
||||
AngularPollChannel, ApiService, Restangular) {
|
||||
|
||||
var result = $element.find('#copyButton').clipboardCopy();
|
||||
if (!result) {
|
||||
$element.find('#copyButton').hide();
|
||||
}
|
||||
|
||||
$scope.logEntries = null;
|
||||
$scope.currentParentEntry = null;
|
||||
$scope.logStartIndex = 0;
|
||||
$scope.buildLogsText = '';
|
||||
|
||||
var pollChannel = null;
|
||||
var currentBuild = null;
|
||||
|
||||
var appendToTextLog = function(type, message) {
|
||||
if (type == 'phase') {
|
||||
text = 'Starting phase: ' + message + '\n';
|
||||
} else {
|
||||
text = message + '\n';
|
||||
}
|
||||
|
||||
$scope.buildLogsText += text.replace(new RegExp("\\033\\[[^m]+m"), '');
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
appendToTextLog(type, entry['message']);
|
||||
}
|
||||
|
||||
return endIndex;
|
||||
};
|
||||
|
||||
var getBuildStatusAndLogs = function(build, callback) {
|
||||
var params = {
|
||||
'repository': build.repository.namespace + '/' + build.repository.name,
|
||||
'build_uuid': build.id
|
||||
};
|
||||
|
||||
ApiService.getRepoBuildStatus(null, params, true).then(function(resp) {
|
||||
if (resp.id != $scope.build.id) { callback(false); return; }
|
||||
|
||||
// Call the build updated handler.
|
||||
$scope.buildUpdated({'build': resp});
|
||||
|
||||
// Save the current build.
|
||||
currentBuild = resp;
|
||||
|
||||
// Load the updated logs for the build.
|
||||
var options = {
|
||||
'start': $scope.logStartIndex
|
||||
};
|
||||
|
||||
ApiService.getRepoBuildLogsAsResource(params, true).withOptions(options).get(function(resp) {
|
||||
// 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 startWatching = function(build) {
|
||||
// Create a new channel for polling the build status and logs.
|
||||
var conductStatusAndLogRequest = function(callback) {
|
||||
getBuildStatusAndLogs(build, callback);
|
||||
};
|
||||
|
||||
pollChannel = AngularPollChannel.create($scope, conductStatusAndLogRequest, 5 * 1000 /* 5s */);
|
||||
pollChannel.start();
|
||||
};
|
||||
|
||||
var stopWatching = function() {
|
||||
if (pollChannel) {
|
||||
pollChannel.stop();
|
||||
pollChannel = null;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('useTimestamps', function() {
|
||||
if (!$scope.logEntries) { return; }
|
||||
$scope.logEntries = $scope.logEntries.slice();
|
||||
});
|
||||
|
||||
$scope.$watch('build', function(build) {
|
||||
if (build) {
|
||||
startWatching(build);
|
||||
} else {
|
||||
stopWatching();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.hasLogs = function(container) {
|
||||
return container.logs.hasEntries;
|
||||
};
|
||||
|
||||
$scope.formatDatetime = function(datetimeString) {
|
||||
var dt = new Date(datetimeString);
|
||||
return dt.toLocaleString();
|
||||
}
|
||||
|
||||
$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;
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -23,6 +23,9 @@ $.fn.clipboardCopy = function() {
|
|||
ZeroClipboard.on('aftercopy', function(e) {
|
||||
var container = e.target.parentNode.parentNode.parentNode;
|
||||
var message = $(container).find('.clipboard-copied-message')[0];
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Resets the animation.
|
||||
var elem = message;
|
||||
|
|
|
@ -11,9 +11,29 @@ angular.module('quay').directive('resourceView', function () {
|
|||
restrict: 'C',
|
||||
scope: {
|
||||
'resource': '=resource',
|
||||
'resources': '=resources',
|
||||
'errorMessage': '=errorMessage'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.getState = function() {
|
||||
if (!$scope.resources && !$scope.resource) {
|
||||
return 'loading';
|
||||
}
|
||||
|
||||
var resources = $scope.resources || [$scope.resource];
|
||||
for (var i = 0; i < resources.length; ++i) {
|
||||
var current = resources[i];
|
||||
if (current.loading) {
|
||||
return 'loading';
|
||||
}
|
||||
|
||||
if (current.error) {
|
||||
return 'error';
|
||||
}
|
||||
}
|
||||
|
||||
return 'ready';
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
|
|
22
static/js/directives/ui/source-commit-link.js
Normal file
22
static/js/directives/ui/source-commit-link.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* An element which displays a link to a commit in Git or Mercurial or another source control.
|
||||
*/
|
||||
angular.module('quay').directive('sourceCommitLink', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/source-commit-link.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'commitSha': '=commitSha',
|
||||
'urlTemplate': '=urlTemplate'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.getUrl = function(sha, template) {
|
||||
return template.replace('{sha}', sha);
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
41
static/js/directives/ui/source-ref-link.js
Normal file
41
static/js/directives/ui/source-ref-link.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* An element which displays a link to a branch or tag in source control.
|
||||
*/
|
||||
angular.module('quay').directive('sourceRefLink', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/source-ref-link.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'ref': '=ref',
|
||||
'branchTemplate': '=branchTemplate',
|
||||
'tagTemplate': '=tagTemplate'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.getKind = function(ref) {
|
||||
var parts = (ref || '').split('/');
|
||||
if (parts.length < 3) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return parts[1];
|
||||
};
|
||||
|
||||
$scope.getTitle = function(ref) {
|
||||
var parts = (ref || '').split('/');
|
||||
if (parts.length < 3) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return parts[2];
|
||||
};
|
||||
|
||||
$scope.getUrl = function(ref, template, kind) {
|
||||
return template.replace('{' + kind + '}', $scope.getTitle(ref));
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* An element which displays information about a build trigger.
|
||||
* DEPRECATED: An element which displays information about a build trigger.
|
||||
*/
|
||||
angular.module('quay').directive('triggerDescription', function () {
|
||||
var directiveDefinitionObject = {
|
||||
|
|
21
static/js/directives/ui/triggered-build-description.js
Normal file
21
static/js/directives/ui/triggered-build-description.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* An element which displays information about a build that was triggered from an outside source.
|
||||
*/
|
||||
angular.module('quay').directive('triggeredBuildDescription', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/triggered-build-description.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'build': '=build'
|
||||
},
|
||||
controller: function($scope, $element, KeyService, TriggerService) {
|
||||
$scope.getGitHubRepoURL = function(build) {
|
||||
return KeyService['githubTriggerEndpoint'] + build.trigger.config.build_source + '/';
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
75
static/js/pages/build-view.js
Normal file
75
static/js/pages/build-view.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
(function() {
|
||||
/**
|
||||
* Build view page. Displays the view of a particular build for a repository.
|
||||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('build-view', 'build-view.html', BuildViewCtrl, {
|
||||
newLayout: true,
|
||||
title: 'Build {{ build.display_name }}',
|
||||
description: 'Logs and status for build {{ build.display_name }}'
|
||||
});
|
||||
}]);
|
||||
|
||||
function BuildViewCtrl($scope, ApiService, $routeParams, AngularPollChannel, CookieService) {
|
||||
$scope.namespace = $routeParams.namespace;
|
||||
$scope.name = $routeParams.name;
|
||||
$scope.build_uuid = $routeParams.buildid;
|
||||
|
||||
$scope.showLogTimestamps = CookieService.get('quay.showBuildLogTimestamps') == 'true';
|
||||
|
||||
var loadBuild = function() {
|
||||
var params = {
|
||||
'repository': $scope.namespace + '/' + $scope.name,
|
||||
'build_uuid': $scope.build_uuid
|
||||
};
|
||||
|
||||
$scope.buildResource = ApiService.getRepoBuildAsResource(params).get(function(build) {
|
||||
$scope.build = build;
|
||||
$scope.originalBuild = build;
|
||||
});
|
||||
};
|
||||
|
||||
var loadRepository = function() {
|
||||
var params = {
|
||||
'repository': $scope.namespace + '/' + $scope.name
|
||||
};
|
||||
|
||||
$scope.repoResource = ApiService.getRepoAsResource(params).get(function(repo) {
|
||||
$scope.repo = repo;
|
||||
}, ApiService.errorDisplay('Cannot load repository'));
|
||||
};
|
||||
|
||||
// Page startup:
|
||||
loadRepository();
|
||||
loadBuild();
|
||||
|
||||
$scope.askCancelBuild = function(build) {
|
||||
bootbox.confirm('Are you sure you want to cancel this build?', function(r) {
|
||||
if (r) {
|
||||
var params = {
|
||||
'repository': $scope.namespace + '/' + $scope.name,
|
||||
'build_uuid': build.id
|
||||
};
|
||||
|
||||
ApiService.cancelRepoBuild(null, params).then(function() {
|
||||
document.location = '/repository/' + $scope.namespace + '/' + $scope.name;
|
||||
}, ApiService.errorDisplay('Cannot cancel build'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.toggleTimestamps = function() {
|
||||
$scope.showLogTimestamps = !$scope.showLogTimestamps;
|
||||
CookieService.putPermanent('quay.showBuildLogTimestamps', $scope.showLogTimestamps);
|
||||
};
|
||||
|
||||
$scope.setUpdatedBuild = function(build) {
|
||||
$scope.build = build;
|
||||
};
|
||||
|
||||
$scope.isBuilding = function(build) {
|
||||
if (!build) { return true; }
|
||||
return build.phase != 'complete' && build.phase != 'error';
|
||||
};
|
||||
}
|
||||
})();
|
76
static/js/services/meta-service.js
Normal file
76
static/js/services/meta-service.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* Service which helps set the contents of the <meta> tags (and the <title> of a page).
|
||||
*/
|
||||
angular.module('quay').factory('MetaService', ['$interpolate', 'Config', '$rootScope', '$interval',
|
||||
function($interpolate, Config, $rootScope, $interval) {
|
||||
var metaService = {};
|
||||
var intervals = [];
|
||||
|
||||
var interpolate = function(page, expr, callback) {
|
||||
var previous = '';
|
||||
|
||||
var currentInterval = $interval(function() {
|
||||
var inter = $interpolate(expr, true, null, true);
|
||||
var result = inter(page.scope)
|
||||
|
||||
if (result != previous) {
|
||||
$interval.cancel(currentInterval);
|
||||
}
|
||||
|
||||
previous = result;
|
||||
callback(result);
|
||||
}, 500);
|
||||
|
||||
intervals.push(currentInterval);
|
||||
};
|
||||
|
||||
var initial = function(value, default_value, callback) {
|
||||
for (var i = 0; i < intervals.length; ++i) {
|
||||
$interval.cancel(intervals[i]);
|
||||
}
|
||||
|
||||
intervals = [];
|
||||
|
||||
if (!value) {
|
||||
callback(default_value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.indexOf('{{') < 0) {
|
||||
callback(default_value);
|
||||
return;
|
||||
}
|
||||
|
||||
callback('Loading...');
|
||||
};
|
||||
|
||||
metaService.getInitialTitle = function(page, callback) {
|
||||
var route = page.$$route;
|
||||
initial(route && route.title, Config.REGISTRY_TITLE_SHORT, callback);
|
||||
};
|
||||
|
||||
metaService.getInitialDescription = function(page, callback) {
|
||||
var route = page.$$route;
|
||||
initial(route && route.description, Config.REGISTRY_TITLE_SHORT, callback);
|
||||
};
|
||||
|
||||
metaService.getTitle = function(page, callback) {
|
||||
var route = page.$$route;
|
||||
if (!route || !route.title || route.title.indexOf('{{') < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
interpolate(page, route.title, callback);
|
||||
};
|
||||
|
||||
metaService.getDescription = function(page, callback) {
|
||||
var route = page.$$route;
|
||||
if (!route || !route.description || route.description.indexOf('{{') < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
interpolate(page, route.description, callback);
|
||||
};
|
||||
|
||||
return metaService;
|
||||
}]);
|
Reference in a new issue