Merge branch 'master' into tutorial
Conflicts: config.py static/js/app.js test/data/test.db
This commit is contained in:
commit
ade20952e2
38 changed files with 1140 additions and 224 deletions
|
@ -507,35 +507,103 @@ i.toggle-icon:hover {
|
|||
color: #428bca;
|
||||
}
|
||||
|
||||
.status-boxes .popover {
|
||||
margin-right: 20px;
|
||||
.status-box a {
|
||||
padding: 6px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.status-boxes .popover-content {
|
||||
width: 260px;
|
||||
.status-box a b {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.build-statuses {
|
||||
.build-info {
|
||||
margin: 4px;
|
||||
padding: 4px;
|
||||
margin-left: 6px;
|
||||
margin-right: 6px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.build-info.clickable:hover {
|
||||
background: rgba(66, 139, 202, 0.2);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
|
||||
.build-info:last-child {
|
||||
border-bottom: 0px;
|
||||
}
|
||||
|
||||
.phase-icon {
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.active .build-tab-link .phase-icon {
|
||||
box-shadow: 0px 0px 10px #FFFFFF, 0px 0px 10px #FFFFFF;
|
||||
}
|
||||
|
||||
.build-status .phase-icon {
|
||||
margin-top: 4px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.phase-icon.error {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.phase-icon.waiting, .phase-icon.starting, .phase-icon.initializing {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
.phase-icon.building {
|
||||
background-color: #f0ad4e;
|
||||
}
|
||||
|
||||
.phase-icon.pushing {
|
||||
background-color: #5cb85c;
|
||||
}
|
||||
|
||||
.phase-icon.complete {
|
||||
background-color: #428bca;
|
||||
}
|
||||
|
||||
.build-status {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.build-status-container {
|
||||
padding: 4px;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
width: 230px;
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
.build-status-container .build-message {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.build-status-container .progress {
|
||||
height: 12px;
|
||||
height: 10px;
|
||||
margin: 0px;
|
||||
margin-top: 10px;
|
||||
width: 230px;
|
||||
margin-left: 20px;
|
||||
width: 310px;
|
||||
}
|
||||
|
||||
.build-status-container .timing {
|
||||
margin-left: 20px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.build-status-container:last-child {
|
||||
|
@ -1633,6 +1701,185 @@ p.editable:hover i {
|
|||
padding-left: 44px;
|
||||
}
|
||||
|
||||
|
||||
.repo-build .build-id:before {
|
||||
content: "Build ID: "
|
||||
}
|
||||
|
||||
.repo-build .build-id {
|
||||
float: right;
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .timing {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.repo-build .build-tab-link {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-header {
|
||||
padding-top: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-progress {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-progress .progress {
|
||||
height: 14px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .quay-spinner {
|
||||
margin-top: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs {
|
||||
background: #222;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .container-header {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .container-logs {
|
||||
margin: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .command-title,
|
||||
.repo-build .build-pane .build-logs .log-entry .message {
|
||||
font-family: Consolas, "Lucida Console", Monaco, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .container-header {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .container-header i.fa.chevron {
|
||||
color: #666;
|
||||
margin-right: 4px;
|
||||
width: 14px;
|
||||
text-align: center;
|
||||
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .log-container.command {
|
||||
margin-left: 42px;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .container-header.building {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .container-header.pushing {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.repo-build .build-log-error-element {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
background: rgba(255, 0, 0, 0.17);
|
||||
border-radius: 10px;
|
||||
margin-left: 22px;
|
||||
}
|
||||
|
||||
.repo-build .build-log-error-element i.fa {
|
||||
color: red;
|
||||
position: absolute;
|
||||
top: 13px;
|
||||
left: 11px;
|
||||
}
|
||||
|
||||
.repo-build .build-log-error-element .error-message {
|
||||
display: inline-block;
|
||||
margin-left: 25px;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .container-header .label {
|
||||
padding-top: 4px;
|
||||
text-align: right;
|
||||
margin-right: 4px;
|
||||
width: 86px;
|
||||
display: inline-block;
|
||||
|
||||
border-right: 4px solid #aaa;
|
||||
background-color: #444;
|
||||
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 24px;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .container-header .container-content {
|
||||
display: block;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .container-header .container-content.build-log-command {
|
||||
padding-left: 120px;
|
||||
}
|
||||
|
||||
.label.FROM {
|
||||
border-color: #5bc0de !important;
|
||||
}
|
||||
|
||||
.label.CMD, .label.EXPOSE, .label.ENTRYPOINT {
|
||||
border-color: #428bca !important;
|
||||
}
|
||||
|
||||
.label.RUN, .label.ADD {
|
||||
border-color: #5cb85c !important;
|
||||
}
|
||||
|
||||
.label.ENV, .label.VOLUME, .label.USER, .label.WORKDIR {
|
||||
border-color: #f0ad4e !important;
|
||||
}
|
||||
|
||||
.label.MAINTAINER {
|
||||
border-color: #aaa !important;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .log-entry {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .log-entry .message {
|
||||
display: inline-block;
|
||||
margin-left: 46px;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .log-entry .id {
|
||||
color: #aaa;
|
||||
padding-right: 6px;
|
||||
margin-right: 6px;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
width: 40px;
|
||||
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
}
|
||||
|
||||
.repo-admin .right-info {
|
||||
font-size: 11px;
|
||||
margin-top: 10px;
|
||||
|
@ -1676,16 +1923,6 @@ p.editable:hover i {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.repo .build-info {
|
||||
padding: 10px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.repo .build-info .progress {
|
||||
margin: 0px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.repo .section {
|
||||
display: block;
|
||||
margin-bottom: 20px;
|
||||
|
@ -2935,4 +3172,4 @@ pre.command:before {
|
|||
|
||||
.about-basic-text {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
|
6
static/directives/build-log-command.html
Normal file
6
static/directives/build-log-command.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
<span class="command" bindonce>
|
||||
<span class="label" bo-class="getCommandKind(command.message)" bo-show="getCommandKind(command.message)"
|
||||
bo-text="getCommandKind(command.message)">
|
||||
</span>
|
||||
<span class="command-title" bo-html="getCommandTitleHtml(command.message)"></span>
|
||||
</span>
|
4
static/directives/build-log-error.html
Normal file
4
static/directives/build-log-error.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
<span bindonce class="build-log-error-element">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
<span class="error-message" bo-text="error.message"></span>
|
||||
</span>
|
4
static/directives/build-log-phase.html
Normal file
4
static/directives/build-log-phase.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
<span bindonce class="build-log-phase-element">
|
||||
<span class="phase-icon" ng-class="phase.message"></span>
|
||||
<span class="build-message" phase="phase.message"></span>
|
||||
</span>
|
1
static/directives/build-message.html
Normal file
1
static/directives/build-message.html
Normal file
|
@ -0,0 +1 @@
|
|||
<span class="build-message-element">{{ getBuildMessage(phase) }}</span>
|
6
static/directives/build-progress.html
Normal file
6
static/directives/build-progress.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
<div class="build-progress-element">
|
||||
<div class="progress" ng-class="getPercentage(build) < 100 ? 'active progress-striped' : ''">
|
||||
<div class="progress-bar" role="progressbar" aria-valuenow="{{ getPercentage(build) }}" aria-valuemin="0" aria-valuemax="100" style="{{ 'width: ' + getPercentage(build) + '%' }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,8 +1,11 @@
|
|||
<div id="build-status-container" class="build-status-container">
|
||||
<span class="build-message">{{ getBuildMessage(build) }}</span>
|
||||
<div class="progress" ng-class="getBuildProgress(build) < 100 ? 'active progress-striped' : ''" ng-show="getBuildProgress(build) >= 0">
|
||||
<div class="progress-bar" role="progressbar" aria-valuenow="{{ getBuildProgress(build) }}" aria-valuemin="0" aria-valuemax="100" style="{{ 'width: ' + getBuildProgress(build) + '%' }}">
|
||||
</div>
|
||||
<div>
|
||||
<span class="phase-icon" ng-class="build.phase"></span>
|
||||
<span class="build-message" phase="build.phase"></span>
|
||||
</div>
|
||||
|
||||
<div class="timing">
|
||||
<i class="fa fa-clock-o"></i>
|
||||
Started: <span am-time-ago="build.started || 0"></span>
|
||||
</div>
|
||||
<div class="build-progress" build="build"></div>
|
||||
</div>
|
||||
|
|
268
static/js/app.js
268
static/js/app.js
|
@ -102,9 +102,8 @@ function getMarkedDown(string) {
|
|||
return Markdown.getSanitizingConverter().makeHtml(string || '');
|
||||
}
|
||||
|
||||
// Start the application code itself.
|
||||
quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', '$strap.directives', 'ngCookies', 'ngSanitize', 'angular-md5'], function($provide, cfpLoadingBarProvider) {
|
||||
cfpLoadingBarProvider.includeSpinner = false;
|
||||
quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', '$strap.directives', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce'], function($provide, cfpLoadingBarProvider) {
|
||||
cfpLoadingBarProvider.includeSpinner = false;
|
||||
|
||||
$provide.factory('UtilService', ['$sanitize', function($sanitize) {
|
||||
var utilService = {};
|
||||
|
@ -151,7 +150,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
$provide.factory('ApiService', ['Restangular', function(Restangular) {
|
||||
var apiService = {};
|
||||
|
||||
var getResource = function(path) {
|
||||
var getResource = function(path, opt_background) {
|
||||
var resource = {};
|
||||
resource.url = path;
|
||||
resource.withOptions = function(options) {
|
||||
|
@ -169,6 +168,12 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
'hasError': false
|
||||
};
|
||||
|
||||
if (opt_background) {
|
||||
performer.withHttpConfig({
|
||||
'ignoreLoadingBar': true
|
||||
});
|
||||
}
|
||||
|
||||
performer.get(options).then(function(resp) {
|
||||
result.value = processor(resp);
|
||||
result.loading = false;
|
||||
|
@ -240,27 +245,33 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
var buildMethodsForEndpoint = function(endpoint) {
|
||||
var method = endpoint.methods[0].toLowerCase();
|
||||
var methodName = formatMethodName(endpoint['name']);
|
||||
apiService[methodName] = function(opt_options, opt_parameters) {
|
||||
return Restangular.one(buildUrl(endpoint['path'], opt_parameters))['custom' + method.toUpperCase()](opt_options);
|
||||
apiService[methodName] = function(opt_options, opt_parameters, opt_background) {
|
||||
var one = Restangular.one(buildUrl(endpoint['path'], opt_parameters));
|
||||
if (opt_background) {
|
||||
one.withHttpConfig({
|
||||
'ignoreLoadingBar': true
|
||||
});
|
||||
}
|
||||
return one['custom' + method.toUpperCase()](opt_options);
|
||||
};
|
||||
|
||||
if (method == 'get') {
|
||||
apiService[methodName + 'AsResource'] = function(opt_parameters) {
|
||||
return getResource(buildUrl(endpoint['path'], opt_parameters));
|
||||
apiService[methodName + 'AsResource'] = function(opt_parameters, opt_background) {
|
||||
return getResource(buildUrl(endpoint['path'], opt_parameters), opt_background);
|
||||
};
|
||||
}
|
||||
|
||||
if (endpoint['user_method']) {
|
||||
apiService[getGenericMethodName(endpoint['user_method'])] = function(orgname, opt_options, opt_parameters) {
|
||||
apiService[getGenericMethodName(endpoint['user_method'])] = function(orgname, opt_options, opt_parameters, opt_background) {
|
||||
if (orgname) {
|
||||
if (orgname.name) {
|
||||
orgname = orgname.name;
|
||||
}
|
||||
|
||||
var params = jQuery.extend({'orgname' : orgname}, opt_parameters || {});
|
||||
var params = jQuery.extend({'orgname' : orgname}, opt_parameters || {}, opt_background);
|
||||
return apiService[methodName](opt_options, params);
|
||||
} else {
|
||||
return apiService[formatMethodName(endpoint['user_method'])](opt_options, opt_parameters);
|
||||
return apiService[formatMethodName(endpoint['user_method'])](opt_options, opt_parameters, opt_background);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -779,6 +790,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
fixFooter: false}).
|
||||
when('/repository/:namespace/:name/image/:image', {templateUrl: '/static/partials/image-view.html', controller: ImageViewCtrl, reloadOnSearch: false}).
|
||||
when('/repository/:namespace/:name/admin', {templateUrl: '/static/partials/repo-admin.html', controller:RepoAdminCtrl, reloadOnSearch: false}).
|
||||
when('/repository/:namespace/:name/build', {templateUrl: '/static/partials/repo-build.html', controller:RepoBuildCtrl, reloadOnSearch: false}).
|
||||
when('/repository/', {title: 'Repositories', description: 'Public and private docker repositories list',
|
||||
templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}).
|
||||
when('/user/', {title: 'Account Settings', description:'Account settings for Quay.io', templateUrl: '/static/partials/user-admin.html',
|
||||
|
@ -2471,6 +2483,119 @@ quayApp.directive('namespaceSelector', function () {
|
|||
});
|
||||
|
||||
|
||||
quayApp.directive('buildLogPhase', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-log-phase.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'phase': '=phase'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('buildLogError', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-log-error.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'error': '=error'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('buildLogCommand', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-log-command.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'command': '=command'
|
||||
},
|
||||
controller: function($scope, $element, $sanitize) {
|
||||
var registryHandlers = {
|
||||
'quay.io': function(pieces) {
|
||||
var rnamespace = pieces[pieces.length - 2];
|
||||
var rname = pieces[pieces.length - 1];
|
||||
return '/repository/' + rnamespace + '/' + rname + '/';
|
||||
},
|
||||
|
||||
'': function(pieces) {
|
||||
var rnamespace = pieces.length == 1 ? '_' : pieces[0];
|
||||
var rname = pieces[pieces.length - 1];
|
||||
return 'https://index.docker.io/u/' + rnamespace + '/' + rname + '/';
|
||||
}
|
||||
};
|
||||
|
||||
var kindHandlers = {
|
||||
'FROM': function(title) {
|
||||
var pieces = title.split('/');
|
||||
var registry = pieces.length < 3 ? '' : pieces[0];
|
||||
if (!registryHandlers[registry]) {
|
||||
return title;
|
||||
}
|
||||
|
||||
return '<i class="fa fa-hdd-o"></i> <a href="' + registryHandlers[registry](pieces) + '">' + title + '</a>';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getCommandKind = function(fullTitle) {
|
||||
var colon = fullTitle.indexOf(':');
|
||||
var title = getTitleWithoutStep(fullTitle);
|
||||
if (!title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var space = title.indexOf(' ');
|
||||
return title.substring(0, space);
|
||||
};
|
||||
|
||||
$scope.getCommandTitleHtml = function(fullTitle) {
|
||||
var title = getTitleWithoutStep(fullTitle) || fullTitle;
|
||||
var space = title.indexOf(' ');
|
||||
if (space <= 0) {
|
||||
return $sanitize(title);
|
||||
}
|
||||
|
||||
var kind = $scope.getCommandKind(fullTitle);
|
||||
var sanitized = $sanitize(title.substring(space + 1));
|
||||
|
||||
var handler = kindHandlers[kind || ''];
|
||||
if (handler) {
|
||||
return handler(sanitized);
|
||||
} else {
|
||||
return sanitized;
|
||||
}
|
||||
};
|
||||
|
||||
var getTitleWithoutStep = function(fullTitle) {
|
||||
var colon = fullTitle.indexOf(':');
|
||||
if (colon <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $.trim(fullTitle.substring(colon + 1));
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
quayApp.directive('buildStatus', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
|
@ -2482,55 +2607,85 @@ quayApp.directive('buildStatus', function () {
|
|||
'build': '=build'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.getBuildProgress = function(buildInfo) {
|
||||
switch (buildInfo.phase) {
|
||||
case 'building':
|
||||
return (buildInfo.status.current_command / buildInfo.status.total_commands) * 100;
|
||||
break;
|
||||
|
||||
case 'pushing':
|
||||
return buildInfo.status.push_completion * 100;
|
||||
break;
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
case 'complete':
|
||||
return 100;
|
||||
break;
|
||||
|
||||
case 'initializing':
|
||||
case 'starting':
|
||||
case 'waiting':
|
||||
return 0;
|
||||
break;
|
||||
}
|
||||
quayApp.directive('buildMessage', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-message.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'phase': '=phase'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.getBuildMessage = function (phase) {
|
||||
switch (phase) {
|
||||
case 'starting':
|
||||
case 'initializing':
|
||||
return 'Starting Dockerfile build';
|
||||
|
||||
case 'waiting':
|
||||
return 'Waiting for available build worker';
|
||||
|
||||
case 'building':
|
||||
return 'Building image from Dockerfile';
|
||||
|
||||
case 'pushing':
|
||||
return 'Pushing image built from Dockerfile';
|
||||
|
||||
return -1;
|
||||
};
|
||||
case 'complete':
|
||||
return 'Dockerfile build completed and pushed';
|
||||
|
||||
case 'error':
|
||||
return 'Dockerfile build failed';
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
$scope.getBuildMessage = function(buildInfo) {
|
||||
switch (buildInfo.phase) {
|
||||
case 'initializing':
|
||||
return 'Starting Dockerfile build';
|
||||
break;
|
||||
|
||||
case 'starting':
|
||||
case 'waiting':
|
||||
case 'building':
|
||||
return 'Building image from Dockerfile';
|
||||
break;
|
||||
quayApp.directive('buildProgress', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-progress.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'build': '=build'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.getPercentage = function(buildInfo) {
|
||||
switch (buildInfo.phase) {
|
||||
case 'building':
|
||||
return (buildInfo.status.current_command / buildInfo.status.total_commands) * 100;
|
||||
break;
|
||||
|
||||
case 'pushing':
|
||||
return buildInfo.status.push_completion * 100;
|
||||
break;
|
||||
|
||||
case 'pushing':
|
||||
return 'Pushing image built from Dockerfile';
|
||||
break;
|
||||
case 'complete':
|
||||
return 100;
|
||||
break;
|
||||
|
||||
case 'complete':
|
||||
return 'Dockerfile build completed and pushed';
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
return 'Dockerfile build failed.';
|
||||
break;
|
||||
}
|
||||
};
|
||||
case 'initializing':
|
||||
case 'starting':
|
||||
case 'waiting':
|
||||
return 0;
|
||||
break;
|
||||
}
|
||||
|
||||
return -1;
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
|
@ -2545,6 +2700,13 @@ quayApp.directive('ngBlur', function() {
|
|||
};
|
||||
});
|
||||
|
||||
quayApp.directive('ngVisible', function () {
|
||||
return function (scope, element, attr) {
|
||||
scope.$watch(attr.ngVisible, function (visible) {
|
||||
element.css('visibility', visible ? 'visible' : 'hidden');
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanService', '$http', '$timeout',
|
||||
function($location, $rootScope, Restangular, UserService, PlanService, $http, $timeout) {
|
||||
|
|
|
@ -326,6 +326,11 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
|||
|
||||
$scope.getFormattedCommand = ImageMetadataService.getFormattedCommand;
|
||||
|
||||
$scope.showBuild = function(buildInfo) {
|
||||
$location.path('/repository/' + namespace + '/' + name + '/build');
|
||||
$location.search('current', buildInfo.id);
|
||||
};
|
||||
|
||||
$scope.getTooltipCommand = function(image) {
|
||||
var sanitized = ImageMetadataService.getEscapedFormattedCommand(image);
|
||||
return '<span class=\'codetooltip\'>' + sanitized + '</span>';
|
||||
|
@ -653,13 +658,11 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
|||
};
|
||||
|
||||
var getBuildInfo = function(repo) {
|
||||
// Note: We use restangular manually here because we need to turn off the loading bar.
|
||||
var buildInfo = Restangular.one('repository/' + repo.namespace + '/' + repo.name + '/build/');
|
||||
buildInfo.withHttpConfig({
|
||||
'ignoreLoadingBar': true
|
||||
});
|
||||
var params = {
|
||||
'repository': repo.namespace + '/' + repo.name
|
||||
};
|
||||
|
||||
buildInfo.get().then(function(resp) {
|
||||
ApiService.getRepoBuilds(null, params, true).then(function(resp) {
|
||||
var runningBuilds = [];
|
||||
for (var i = 0; i < resp.builds.length; ++i) {
|
||||
var build = resp.builds[i];
|
||||
|
@ -745,6 +748,197 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
|||
loadViewInfo();
|
||||
}
|
||||
|
||||
function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $interval, $sanitize) {
|
||||
var namespace = $routeParams.namespace;
|
||||
var name = $routeParams.name;
|
||||
var pollTimerHandle = null;
|
||||
|
||||
$scope.$on('$destroy', function() {
|
||||
stopPollTimer();
|
||||
});
|
||||
|
||||
// Watch for changes to the current parameter.
|
||||
$scope.$on('$routeUpdate', function(){
|
||||
if ($location.search().current) {
|
||||
$scope.setCurrentBuild($location.search().current, false);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.builds = [];
|
||||
$scope.polling = false;
|
||||
|
||||
$scope.adjustLogHeight = function() {
|
||||
$('.build-logs').height($(window).height() - 365);
|
||||
};
|
||||
|
||||
$scope.hasLogs = function(container) {
|
||||
return ((container.logs && container.logs.length) || (container._logs && container._logs.length));
|
||||
};
|
||||
|
||||
$scope.toggleLogs = function(container) {
|
||||
if (container._logs) {
|
||||
container.logs = container._logs;
|
||||
container._logs = null;
|
||||
} else {
|
||||
container._logs = container.logs;
|
||||
container.logs = null;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.setCurrentBuild = function(buildId, opt_updateURL) {
|
||||
// Find the build.
|
||||
for (var i = 0; i < $scope.builds.length; ++i) {
|
||||
if ($scope.builds[i].id == buildId) {
|
||||
$scope.setCurrentBuildInternal($scope.builds[i], opt_updateURL);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.setCurrentBuildInternal = function(build, opt_updateURL) {
|
||||
if (build == $scope.currentBuild) { return; }
|
||||
|
||||
stopPollTimer();
|
||||
|
||||
$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);
|
||||
|
||||
// Load the first set of logs.
|
||||
getBuildStatusAndLogs();
|
||||
|
||||
// If the build is currently processing, start the build timer.
|
||||
checkPollTimer();
|
||||
};
|
||||
|
||||
var checkPollTimer = function() {
|
||||
var build = $scope.currentBuild;
|
||||
if (!build) {
|
||||
stopPollTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (build['phase'] != 'complete' && build['phase'] != 'error') {
|
||||
startPollTimer();
|
||||
return true;
|
||||
} else {
|
||||
stopPollTimer();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
var stopPollTimer = function() {
|
||||
$interval.cancel(pollTimerHandle);
|
||||
};
|
||||
|
||||
var startPollTimer = function() {
|
||||
stopPollTimer();
|
||||
pollTimerHandle = $interval(getBuildStatusAndLogs, 2000);
|
||||
};
|
||||
|
||||
var processLogs = function(logs, startIndex) {
|
||||
if (!$scope.logEntries) { $scope.logEntries = []; }
|
||||
|
||||
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'] = [];
|
||||
entry['index'] = startIndex + i;
|
||||
|
||||
$scope.logEntries.push(entry);
|
||||
$scope.currentParentEntry = entry;
|
||||
} else if ($scope.currentParentEntry) {
|
||||
if ($scope.currentParentEntry['logs']) {
|
||||
$scope.currentParentEntry['logs'].push(entry);
|
||||
} else {
|
||||
$scope.currentParentEntry['_logs'].push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var getBuildStatusAndLogs = function() {
|
||||
if (!$scope.currentBuild || $scope.polling) { return; }
|
||||
|
||||
$scope.polling = true;
|
||||
|
||||
var params = {
|
||||
'repository': namespace + '/' + name,
|
||||
'build_uuid': $scope.currentBuild.id
|
||||
};
|
||||
|
||||
ApiService.getRepoBuildStatus(null, params, true).then(function(resp) {
|
||||
// Note: We use extend here rather than replacing as Angular is depending on the
|
||||
// root build object to remain the same object.
|
||||
$.extend(true, $scope.currentBuild, resp);
|
||||
checkPollTimer();
|
||||
|
||||
// Load the updated logs for the build.
|
||||
var options = {
|
||||
'start': $scope.logStartIndex
|
||||
};
|
||||
|
||||
ApiService.getRepoBuildLogsAsResource(params, true).withOptions(options).get(function(resp) {
|
||||
processLogs(resp['logs'], resp['start']);
|
||||
$scope.logStartIndex = resp['total'];
|
||||
$scope.polling = 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, $routeParams, $rootScope) {
|
||||
var namespace = $routeParams.namespace;
|
||||
var name = $routeParams.name;
|
||||
|
@ -1002,8 +1196,13 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
};
|
||||
|
||||
$scope.repository = ApiService.getRepoAsResource(params).get(function(repo) {
|
||||
$scope.repo = repo;
|
||||
if (!repo.can_admin) {
|
||||
$rootScope.title = 'Forbidden';
|
||||
$scope.accessDenied = true;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.repo = repo;
|
||||
$rootScope.title = 'Settings - ' + namespace + '/' + name;
|
||||
$rootScope.description = 'Administrator settings for ' + namespace + '/' + name +
|
||||
': Permissions, webhooks and other settings';
|
||||
|
|
1
static/lib/bindonce.min.js
vendored
Normal file
1
static/lib/bindonce.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,5 +1,8 @@
|
|||
<div class="container" ng-show="deleting"><div class="quay-spinner"></div></div>
|
||||
<div class="resource-view" resource="repository" error-message="'No repository found'"></div>
|
||||
<div class="container repo repo-admin" ng-show="accessDenied">
|
||||
You do not have permission to view this page
|
||||
</div>
|
||||
<div class="container repo repo-admin" ng-show="repo && !deleting">
|
||||
<div class="header row">
|
||||
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="fa fa-chevron-left"></i></a>
|
||||
|
|
83
static/partials/repo-build.html
Normal file
83
static/partials/repo-build.html
Normal file
|
@ -0,0 +1,83 @@
|
|||
<div class="resource-view" resource="repository" error-message="'No repository found'"></div>
|
||||
<div class="container repo repo-build" ng-show="accessDenied">
|
||||
You do not have permission to view this page
|
||||
</div>
|
||||
<div class="container repo repo-build" ng-show="repo">
|
||||
<div class="header row">
|
||||
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="fa fa-chevron-left"></i></a>
|
||||
<h3>
|
||||
<span class="repo-circle no-background" repo="repo"></span>
|
||||
<span class="repo-breadcrumb" repo="repo"></span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="!builds.length">
|
||||
There are no builds for this repository
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="builds.length">
|
||||
<!-- Side tabs -->
|
||||
<div class="col-sm-2">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li ng-class="currentBuild == build ? 'active' : ''" ng-repeat="build in builds">
|
||||
<a class="build-tab-link" href="javascript:void(0)" ng-click="setCurrentBuild(build.id, true)">
|
||||
<span class="phase-icon" ng-class="build.phase"></span>
|
||||
<span>{{ build.display_name }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="col-sm-10">
|
||||
<div class="tab-content" onresize="adjustLogHeight()">
|
||||
<div ng-repeat="build in builds" class="tab-pane build-pane" ng-class="currentBuild == build ? 'active' : ''">
|
||||
<div class="build-header">
|
||||
<div class="timing">
|
||||
<i class="fa fa-clock-o"></i>
|
||||
Started: <span am-time-ago="build.started || 0"></span>
|
||||
</div>
|
||||
<span class="phase-icon" ng-class="build.phase"></span>
|
||||
<span class="build-message" phase="build.phase"></span>
|
||||
<div class="build-progress" build="build"></div>
|
||||
</div>
|
||||
|
||||
<div class="build-logs">
|
||||
<div ng-show="!logEntries">
|
||||
<span class="quay-spinner"></span>
|
||||
</div>
|
||||
|
||||
<div class="log-container" ng-class="container.type" ng-repeat="container in logEntries">
|
||||
<div class="container-header" ng-class="container.type == 'phase' ? container.message : ''"
|
||||
ng-switch on="container.type" ng-click="toggleLogs(container)">
|
||||
<i class="fa chevron"
|
||||
ng-class="container.logs ? 'fa-chevron-down' : 'fa-chevron-right'" ng-show="hasLogs(container)"></i>
|
||||
<div ng-switch-when="phase">
|
||||
<span class="container-content build-log-phase" phase="container"></span>
|
||||
</div>
|
||||
<div ng-switch-when="error">
|
||||
<span class="container-content build-log-error" error="container"></span>
|
||||
</div>
|
||||
<div ng-switch-when="command">
|
||||
<span class="container-content build-log-command" command="container"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Display the entries for the container -->
|
||||
<div class="container-logs" ng-show="container.logs">
|
||||
<div class="log-entry" bindonce ng-repeat="entry in container.logs">
|
||||
<span class="id" bo-text="$index + container.index + 1"></span>
|
||||
<span class="message" bo-text="entry.message"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="quay-spinner" ng-show="polling"></span>
|
||||
<span class="build-id">{{ build.id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -38,13 +38,18 @@
|
|||
|
||||
<!-- Status boxes -->
|
||||
<div class="status-boxes">
|
||||
<div id="buildInfoBox" class="status-box" ng-show="repo.is_building"
|
||||
bs-popover="'static/partials/build-status-item.html'" data-placement="bottom">
|
||||
<span class="title">
|
||||
<span class="quay-spinner"></span>
|
||||
<b>Building Images</b>
|
||||
</span>
|
||||
<span class="count" ng-class="buildsInfo ? 'visible' : ''"><span>{{ buildsInfo ? buildsInfo.length : '-' }}</span></span>
|
||||
<div id="buildInfoBox" class="status-box" ng-show="repo.is_building">
|
||||
<div class="dropdown" data-placement="top">
|
||||
<span class="count" ng-class="buildsInfo ? 'visible' : ''"><span>{{ buildsInfo ? buildsInfo.length : '-' }}</span></span>
|
||||
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">Building Dockerfile<span ng-show="buildsInfo.length > 1">s</span> <b class="caret"></b></a>
|
||||
<ul class="dropdown-menu pull-right">
|
||||
<li ng-repeat="buildInfo in buildsInfo">
|
||||
<div class="build-info" ng-class="repo.can_write ? 'clickable' : ''" ng-click="showBuild(buildInfo)">
|
||||
<span class="build-status" build="buildInfo"></span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
Reference in a new issue