Merge remote-tracking branch 'origin/master' into allyourbaseimage
This commit is contained in:
commit
46de02a9ec
13 changed files with 882 additions and 216 deletions
|
@ -770,6 +770,16 @@ def get_all_repo_users(namespace_name, repository_name):
|
|||
Repository.name == repository_name)
|
||||
|
||||
|
||||
def get_repository_for_resource(resource_key):
|
||||
joined = Repository.select().join(RepositoryBuild)
|
||||
query = joined.where(RepositoryBuild.resource_key == resource_key).limit(1)
|
||||
result = list(query)
|
||||
if not result:
|
||||
return None
|
||||
|
||||
return result[0]
|
||||
|
||||
|
||||
def get_repository(namespace_name, repository_name):
|
||||
try:
|
||||
return Repository.get(Repository.name == repository_name,
|
||||
|
|
|
@ -1157,6 +1157,7 @@ def build_status_view(build_obj):
|
|||
'started': build_obj.started,
|
||||
'display_name': build_obj.display_name,
|
||||
'status': status,
|
||||
'resource_key': build_obj.resource_key
|
||||
}
|
||||
|
||||
|
||||
|
@ -1221,6 +1222,14 @@ def request_repo_build(namespace, repository):
|
|||
logger.debug('User requested repository initialization.')
|
||||
dockerfile_id = request.get_json()['file_id']
|
||||
|
||||
# Check if the dockerfile resource has already been used. If so, then it can only be reused if the
|
||||
# user has access to the repository for which it was used.
|
||||
associated_repository = model.get_repository_for_resource(dockerfile_id)
|
||||
if associated_repository:
|
||||
if not ModifyRepositoryPermission(associated_repository.namespace, associated_repository.name):
|
||||
abort(403)
|
||||
|
||||
# Start the build.
|
||||
repo = model.get_repository(namespace, repository)
|
||||
token = model.create_access_token(repo, 'write')
|
||||
display_name = user_files.get_file_checksum(dockerfile_id)
|
||||
|
|
|
@ -846,9 +846,19 @@ i.toggle-icon:hover {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.visible-md-inline {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hidden-sm-inline {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 991px) {
|
||||
.visible-md-inline {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 991px) and (min-width: 768px) {
|
||||
.visible-sm-inline {
|
||||
|
@ -1449,7 +1459,10 @@ p.editable:hover i {
|
|||
}
|
||||
|
||||
.repo .header {
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
@ -1499,36 +1512,12 @@ p.editable:hover i {
|
|||
display: inline-block;
|
||||
}
|
||||
|
||||
.repo .status-boxes {
|
||||
float: right;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.repo .status-boxes .status-box {
|
||||
cursor: pointer;
|
||||
.repo .repo-controls .count {
|
||||
display: inline-block;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.repo .status-boxes .status-box .title {
|
||||
padding: 4px;
|
||||
display: inline-block;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.repo .status-boxes .status-box .title i {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.repo .status-boxes .status-box .count {
|
||||
display: inline-block;
|
||||
background-image: linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);
|
||||
padding: 4px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
font-weight: bold;
|
||||
color: #428bca;
|
||||
|
||||
transform: scaleX(0);
|
||||
-webkit-transform: scaleX(0);
|
||||
|
@ -1539,13 +1528,13 @@ p.editable:hover i {
|
|||
-moz-transition: -moz-transform 500ms ease-in-out;
|
||||
}
|
||||
|
||||
.repo .status-boxes .status-box .count.visible {
|
||||
.repo .repo-controls .count.visible {
|
||||
transform: scaleX(1);
|
||||
-webkit-transform: scaleX(1);
|
||||
-moz-transform: scaleX(1);
|
||||
}
|
||||
|
||||
.repo .pull-command {
|
||||
.repo .repo-controls {
|
||||
float: right;
|
||||
display: inline-block;
|
||||
font-size: 0.8em;
|
||||
|
@ -1759,7 +1748,8 @@ p.editable:hover i {
|
|||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .command-title,
|
||||
.repo-build .build-pane .build-logs .log-entry .message {
|
||||
.repo-build .build-pane .build-logs .log-entry .message,
|
||||
.repo-build .build-pane .build-logs .log-entry .message span {
|
||||
font-family: Consolas, "Lucida Console", Monaco, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
@ -3196,4 +3186,9 @@ pre.command:before {
|
|||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.file-drop {
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
}
|
27
static/directives/dockerfile-build-dialog.html
Normal file
27
static/directives/dockerfile-build-dialog.html
Normal file
|
@ -0,0 +1,27 @@
|
|||
<div class="dockerfile-build-dialog-element">
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="dockerfilebuildModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">
|
||||
Start new Dockerfile build
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body token-dialog-body">
|
||||
<div class="alert alert-danger" ng-show="errorMessage">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
<div class="dockerfile-build-form" repository="repository" upload-failed="handleBuildFailed(message)"
|
||||
build-started="handleBuildStarted(build)" build-failed="handleBuildFailed(message)" start-now="startCounter"
|
||||
has-dockerfile="hasDockerfile" uploading="uploading" building="building"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" ng-click="startBuild()" ng-disabled="building || uploading || !hasDockerfile">Start Build</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
</div>
|
19
static/directives/dockerfile-build-form.html
Normal file
19
static/directives/dockerfile-build-form.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
<div class="dockerfile-build-form-element">
|
||||
<div class="container" ng-show="building">
|
||||
<div class="quay-spinner"></div>
|
||||
</div>
|
||||
<div class="container" ng-show="uploading">
|
||||
<span class="message">Uploading file {{ upload_file }}</span>
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar" role="progressbar" aria-valuenow="{{ upload_progress }}" aria-valuemin="0" aria-valuemax="100" style="{{ 'width: ' + upload_progress + '%' }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container" ng-show="!uploading && !building">
|
||||
<div class="init-description">
|
||||
Upload a Dockerfile or a zip file containing a Dockerfile <b>in the root directory</b>
|
||||
</div>
|
||||
<input id="file-drop" class="file-drop" type="file" file-present="internal.hasDockerfile">
|
||||
</div>
|
||||
</div>
|
258
static/js/app.js
258
static/js/app.js
|
@ -102,7 +102,7 @@ function getMarkedDown(string) {
|
|||
return Markdown.getSanitizingConverter().makeHtml(string || '');
|
||||
}
|
||||
|
||||
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) {
|
||||
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', 'ansiToHtml'], function($provide, cfpLoadingBarProvider) {
|
||||
cfpLoadingBarProvider.includeSpinner = false;
|
||||
|
||||
$provide.factory('UtilService', ['$sanitize', function($sanitize) {
|
||||
|
@ -1143,6 +1143,13 @@ quayApp.directive('dockerAuthDialog', function () {
|
|||
});
|
||||
|
||||
|
||||
quayApp.filter('reverse', function() {
|
||||
return function(items) {
|
||||
return items.slice().reverse();
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
quayApp.filter('bytes', function() {
|
||||
return function(bytes, precision) {
|
||||
if (!bytes || isNaN(parseFloat(bytes)) || !isFinite(bytes)) return 'Unknown';
|
||||
|
@ -2693,6 +2700,239 @@ quayApp.directive('buildProgress', function () {
|
|||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('dockerfileBuildDialog', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/dockerfile-build-dialog.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'showNow': '=showNow',
|
||||
'buildStarted': '&buildStarted'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.building = false;
|
||||
$scope.uploading = false;
|
||||
$scope.startCounter = 0;
|
||||
|
||||
$scope.handleBuildStarted = function(build) {
|
||||
$('#dockerfilebuildModal').modal('hide');
|
||||
if ($scope.buildStarted) {
|
||||
$scope.buildStarted({'build': build});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.handleBuildFailed = function(message) {
|
||||
$scope.errorMessage = message;
|
||||
};
|
||||
|
||||
$scope.startBuild = function() {
|
||||
$scope.errorMessage = null;
|
||||
$scope.startCounter++;
|
||||
};
|
||||
|
||||
$scope.$watch('showNow', function(sn) {
|
||||
if (sn && $scope.repository) {
|
||||
$('#dockerfilebuildModal').modal({});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('dockerfileBuildForm', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/dockerfile-build-form.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'startNow': '=startNow',
|
||||
'hasDockerfile': '=hasDockerfile',
|
||||
'uploadFailed': '&uploadFailed',
|
||||
'uploadStarted': '&uploadStarted',
|
||||
'buildStarted': '&buildStarted',
|
||||
'buildFailed': '&buildFailed',
|
||||
'missingFile': '&missingFile',
|
||||
'uploading': '=uploading',
|
||||
'building': '=building'
|
||||
},
|
||||
controller: function($scope, $element, ApiService) {
|
||||
$scope.internal = {'hasDockerfile': false};
|
||||
|
||||
var handleBuildFailed = function(message) {
|
||||
message = message || 'Dockerfile build failed to start';
|
||||
|
||||
var result = false;
|
||||
if ($scope.buildFailed) {
|
||||
result = $scope.buildFailed({'message': message});
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
bootbox.dialog({
|
||||
"message": message,
|
||||
"title": "Cannot start Dockerfile build",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var handleUploadFailed = function(message) {
|
||||
message = message || 'Error with file upload';
|
||||
|
||||
var result = false;
|
||||
if ($scope.uploadFailed) {
|
||||
result = $scope.uploadFailed({'message': message});
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
bootbox.dialog({
|
||||
"message": message,
|
||||
"title": "Cannot upload file for Dockerfile build",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var handleMissingFile = function() {
|
||||
var result = false;
|
||||
if ($scope.missingFile) {
|
||||
result = $scope.missingFile({});
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
bootbox.dialog({
|
||||
"message": 'A Dockerfile or an archive containing a Dockerfile is required',
|
||||
"title": "Missing Dockerfile",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var startBuild = function(fileId) {
|
||||
$scope.building = true;
|
||||
|
||||
var repo = $scope.repository;
|
||||
var data = {
|
||||
'file_id': fileId
|
||||
};
|
||||
|
||||
var params = {
|
||||
'repository': repo.namespace + '/' + repo.name
|
||||
};
|
||||
|
||||
ApiService.requestRepoBuild(data, params).then(function(resp) {
|
||||
$scope.building = false;
|
||||
$scope.uploading = false;
|
||||
|
||||
if ($scope.buildStarted) {
|
||||
$scope.buildStarted({'build': resp});
|
||||
}
|
||||
}, function(resp) {
|
||||
$scope.building = false;
|
||||
$scope.uploading = false;
|
||||
|
||||
handleBuildFailed(resp.message);
|
||||
});
|
||||
};
|
||||
|
||||
var conductUpload = function(file, url, fileId, mimeType) {
|
||||
if ($scope.uploadStarted) {
|
||||
$scope.uploadStarted({});
|
||||
}
|
||||
|
||||
var request = new XMLHttpRequest();
|
||||
request.open('PUT', url, true);
|
||||
request.setRequestHeader('Content-Type', mimeType);
|
||||
request.onprogress = function(e) {
|
||||
$scope.$apply(function() {
|
||||
var percentLoaded;
|
||||
if (e.lengthComputable) {
|
||||
$scope.upload_progress = (e.loaded / e.total) * 100;
|
||||
}
|
||||
});
|
||||
};
|
||||
request.onerror = function() {
|
||||
$scope.$apply(function() {
|
||||
handleUploadFailed();
|
||||
});
|
||||
};
|
||||
request.onreadystatechange = function() {
|
||||
var state = request.readyState;
|
||||
if (state == 4) {
|
||||
$scope.$apply(function() {
|
||||
startBuild(fileId);
|
||||
$scope.uploading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
request.send(file);
|
||||
};
|
||||
|
||||
var startFileUpload = function(repo) {
|
||||
$scope.uploading = true;
|
||||
$scope.uploading_progress = 0;
|
||||
|
||||
var uploader = $('#file-drop')[0];
|
||||
if (uploader.files.length == 0) {
|
||||
handleMissingFile();
|
||||
$scope.uploading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var file = uploader.files[0];
|
||||
$scope.upload_file = file.name;
|
||||
|
||||
var mimeType = file.type || 'application/octet-stream';
|
||||
var data = {
|
||||
'mimeType': mimeType
|
||||
};
|
||||
|
||||
var getUploadUrl = ApiService.getFiledropUrl(data).then(function(resp) {
|
||||
conductUpload(file, resp.url, resp.file_id, mimeType);
|
||||
}, function() {
|
||||
handleUploadFailed('Could not retrieve upload URL');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('internal.hasDockerfile', function(d) {
|
||||
$scope.hasDockerfile = d;
|
||||
});
|
||||
|
||||
$scope.$watch('startNow', function() {
|
||||
if ($scope.startNow && $scope.repository && !$scope.uploading && !$scope.building) {
|
||||
startFileUpload();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
// Note: ngBlur is not yet in Angular stable, so we add it manaully here.
|
||||
quayApp.directive('ngBlur', function() {
|
||||
return function( scope, elem, attrs ) {
|
||||
|
@ -2702,6 +2942,22 @@ quayApp.directive('ngBlur', function() {
|
|||
};
|
||||
});
|
||||
|
||||
quayApp.directive("filePresent", [function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
'filePresent': "="
|
||||
},
|
||||
link: function (scope, element, attributes) {
|
||||
element.bind("change", function (changeEvent) {
|
||||
scope.$apply(function() {
|
||||
scope.filePresent = changeEvent.target.files.length > 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}]);
|
||||
|
||||
quayApp.directive('ngVisible', function () {
|
||||
return function (scope, element, attr) {
|
||||
scope.$watch(attr.ngVisible, function (visible) {
|
||||
|
|
|
@ -341,8 +341,17 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
|||
|
||||
// Start scope methods //////////////////////////////////////////
|
||||
|
||||
$scope.buildDialogShowCounter = 0;
|
||||
$scope.getFormattedCommand = ImageMetadataService.getFormattedCommand;
|
||||
|
||||
$scope.showNewBuildDialog = function() {
|
||||
$scope.buildDialogShowCounter++;
|
||||
};
|
||||
|
||||
$scope.handleBuildStarted = function(build) {
|
||||
startBuildInfoTimer($scope.repo);
|
||||
};
|
||||
|
||||
$scope.showBuild = function(buildInfo) {
|
||||
$location.path('/repository/' + namespace + '/' + name + '/build');
|
||||
$location.search('current', buildInfo.id);
|
||||
|
@ -765,7 +774,7 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
|||
loadViewInfo();
|
||||
}
|
||||
|
||||
function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $interval, $sanitize) {
|
||||
function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $interval, $sanitize, ansi2html) {
|
||||
var namespace = $routeParams.namespace;
|
||||
var name = $routeParams.name;
|
||||
var pollTimerHandle = null;
|
||||
|
@ -784,8 +793,40 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
$scope.builds = [];
|
||||
$scope.polling = false;
|
||||
|
||||
$scope.buildDialogShowCounter = 0;
|
||||
|
||||
$scope.showNewBuildDialog = function() {
|
||||
$scope.buildDialogShowCounter++;
|
||||
};
|
||||
|
||||
$scope.handleBuildStarted = function(newBuild) {
|
||||
$scope.builds.push(newBuild);
|
||||
$scope.setCurrentBuild(newBuild['id'], true);
|
||||
};
|
||||
|
||||
$scope.adjustLogHeight = function() {
|
||||
$('.build-logs').height($(window).height() - 365);
|
||||
$('.build-logs').height($(window).height() - 385);
|
||||
};
|
||||
|
||||
$scope.askRestartBuild = function(build) {
|
||||
$('#confirmRestartBuildModal').modal({});
|
||||
};
|
||||
|
||||
$scope.restartBuild = function(build) {
|
||||
$('#confirmRestartBuildModal').modal('hide');
|
||||
|
||||
var data = {
|
||||
'file_id': build['resource_key']
|
||||
};
|
||||
|
||||
var params = {
|
||||
'repository': namespace + '/' + name
|
||||
};
|
||||
|
||||
ApiService.requestRepoBuild(data, params).then(function(newBuild) {
|
||||
$scope.builds.push(newBuild);
|
||||
$scope.setCurrentBuild(newBuild['id'], true);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.hasLogs = function(container) {
|
||||
|
@ -806,13 +847,23 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
// 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);
|
||||
$scope.setCurrentBuildInternal(i, $scope.builds[i], opt_updateURL);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.setCurrentBuildInternal = function(build, opt_updateURL) {
|
||||
$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; }
|
||||
|
||||
stopPollTimer();
|
||||
|
@ -822,6 +873,7 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
$scope.currentParentEntry = null;
|
||||
|
||||
$scope.currentBuild = build;
|
||||
$scope.currentBuildIndex = index;
|
||||
|
||||
if (opt_updateURL) {
|
||||
if (build) {
|
||||
|
@ -893,7 +945,6 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
|
||||
var getBuildStatusAndLogs = function() {
|
||||
if (!$scope.currentBuild || $scope.polling) { return; }
|
||||
|
||||
$scope.polling = true;
|
||||
|
||||
var params = {
|
||||
|
@ -904,7 +955,7 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
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);
|
||||
$.extend(true, $scope.builds[$scope.currentBuildIndex], resp);
|
||||
checkPollTimer();
|
||||
|
||||
// Load the updated logs for the build.
|
||||
|
@ -913,9 +964,16 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
};
|
||||
|
||||
ApiService.getRepoBuildLogsAsResource(params, true).withOptions(options).get(function(resp) {
|
||||
if ($scope.logStartIndex != null && resp['start'] != $scope.logStartIndex) {
|
||||
$scope.polling = false;
|
||||
return;
|
||||
}
|
||||
|
||||
processLogs(resp['logs'], resp['start']);
|
||||
$scope.logStartIndex = resp['total'];
|
||||
$scope.polling = false;
|
||||
}, function() {
|
||||
$scope.polling = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -948,7 +1006,7 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
if ($location.search().current) {
|
||||
$scope.setCurrentBuild($location.search().current, false);
|
||||
} else if ($scope.builds.length > 0) {
|
||||
$scope.setCurrentBuild($scope.builds[0].id, true);
|
||||
$scope.setCurrentBuild($scope.builds[$scope.builds.length - 1].id, true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -1510,12 +1568,6 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
|
|||
'initialize': false
|
||||
};
|
||||
|
||||
$('#couldnotbuildModal').on('hidden.bs.modal', function() {
|
||||
$scope.$apply(function() {
|
||||
$location.path('/repository/' + $scope.created.namespace + '/' + $scope.created.name);
|
||||
});
|
||||
});
|
||||
|
||||
// Watch the namespace on the repo. If it changes, we update the plan and the public/private
|
||||
// accordingly.
|
||||
$scope.isUserNamespace = true;
|
||||
|
@ -1562,15 +1614,36 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
|
|||
$scope.repo.namespace = namespace;
|
||||
};
|
||||
|
||||
$scope.handleBuildStarted = function() {
|
||||
var repo = $scope.repo;
|
||||
$location.path('/repository/' + repo.namespace + '/' + repo.name);
|
||||
};
|
||||
|
||||
$scope.handleBuildFailed = function(message) {
|
||||
var repo = $scope.repo;
|
||||
|
||||
bootbox.dialog({
|
||||
"message": message,
|
||||
"title": "Could not start Dockerfile build",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary",
|
||||
"callback": function() {
|
||||
$scope.$apply(function() {
|
||||
$location.path('/repository/' + repo.namespace + '/' + repo.name);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
$scope.createNewRepo = function() {
|
||||
$('#repoName').popover('hide');
|
||||
|
||||
var uploader = $('#file-drop')[0];
|
||||
if ($scope.repo.initialize && uploader.files.length < 1) {
|
||||
$('#missingfileModal').modal();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.creating = true;
|
||||
var repo = $scope.repo;
|
||||
var data = {
|
||||
|
@ -1586,7 +1659,7 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
|
|||
|
||||
// Repository created. Start the upload process if applicable.
|
||||
if ($scope.repo.initialize) {
|
||||
startFileUpload(created);
|
||||
$scope.createdForBuild = created;
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1615,74 +1688,6 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
|
|||
|
||||
PlanService.changePlan($scope, null, $scope.planRequired.stripeId, callbacks);
|
||||
};
|
||||
|
||||
var startBuild = function(repo, fileId) {
|
||||
$scope.building = true;
|
||||
|
||||
var data = {
|
||||
'file_id': fileId
|
||||
};
|
||||
|
||||
var params = {
|
||||
'repository': repo.namespace + '/' + repo.name
|
||||
};
|
||||
|
||||
ApiService.requestRepoBuild(data, params).then(function(resp) {
|
||||
$location.path('/repository/' + params.repository);
|
||||
}, function() {
|
||||
$('#couldnotbuildModal').modal();
|
||||
});
|
||||
};
|
||||
|
||||
var conductUpload = function(repo, file, url, fileId, mimeType) {
|
||||
var request = new XMLHttpRequest();
|
||||
request.open('PUT', url, true);
|
||||
request.setRequestHeader('Content-Type', mimeType);
|
||||
request.onprogress = function(e) {
|
||||
$scope.$apply(function() {
|
||||
var percentLoaded;
|
||||
if (e.lengthComputable) {
|
||||
$scope.upload_progress = (e.loaded / e.total) * 100;
|
||||
}
|
||||
});
|
||||
};
|
||||
request.onerror = function() {
|
||||
$scope.$apply(function() {
|
||||
$('#couldnotbuildModal').modal();
|
||||
});
|
||||
};
|
||||
request.onreadystatechange = function() {
|
||||
var state = request.readyState;
|
||||
if (state == 4) {
|
||||
$scope.$apply(function() {
|
||||
$scope.uploading = false;
|
||||
startBuild(repo, fileId);
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
request.send(file);
|
||||
};
|
||||
|
||||
var startFileUpload = function(repo) {
|
||||
$scope.uploading = true;
|
||||
$scope.uploading_progress = 0;
|
||||
|
||||
var uploader = $('#file-drop')[0];
|
||||
var file = uploader.files[0];
|
||||
$scope.upload_file = file.name;
|
||||
|
||||
var mimeType = file.type || 'application/octet-stream';
|
||||
var data = {
|
||||
'mimeType': mimeType
|
||||
};
|
||||
|
||||
var getUploadUrl = ApiService.getFiledropUrl(data).then(function(resp) {
|
||||
conductUpload(repo, file, resp.url, resp.file_id, mimeType);
|
||||
}, function() {
|
||||
$('#couldnotbuildModal').modal();
|
||||
});
|
||||
};
|
||||
|
||||
var subscribedToPlan = function(sub) {
|
||||
$scope.planChanging = false;
|
||||
|
|
335
static/lib/ansi2html.js
Normal file
335
static/lib/ansi2html.js
Normal file
|
@ -0,0 +1,335 @@
|
|||
/**
|
||||
* Originally from: https://github.com/jorgeecardona/ansi-to-html
|
||||
* Modified by jschorr: Add ability to repeat existing styles and not close them out automatically.
|
||||
*/
|
||||
angular.module('ansiToHtml', []).value('ansi2html', (function() {
|
||||
// Define the styles supported from ANSI.
|
||||
var STYLES = {
|
||||
'ef0': 'color:#000',
|
||||
'ef1': 'color:#A00',
|
||||
'ef2': 'color:#0A0',
|
||||
'ef3': 'color:#A50',
|
||||
'ef4': 'color:#00A',
|
||||
'ef5': 'color:#A0A',
|
||||
'ef6': 'color:#0AA',
|
||||
'ef7': 'color:#AAA',
|
||||
'ef8': 'color:#555',
|
||||
'ef9': 'color:#F55',
|
||||
'ef10': 'color:#5F5',
|
||||
'ef11': 'color:#FF5',
|
||||
'ef12': 'color:#55F',
|
||||
'ef13': 'color:#F5F',
|
||||
'ef14': 'color:#5FF',
|
||||
'ef15': 'color:#FFF',
|
||||
'eb0': 'background-color:#000',
|
||||
'eb1': 'background-color:#A00',
|
||||
'eb2': 'background-color:#0A0',
|
||||
'eb3': 'background-color:#A50',
|
||||
'eb4': 'background-color:#00A',
|
||||
'eb5': 'background-color:#A0A',
|
||||
'eb6': 'background-color:#0AA',
|
||||
'eb7': 'background-color:#AAA',
|
||||
'eb8': 'background-color:#555',
|
||||
'eb9': 'background-color:#F55',
|
||||
'eb10': 'background-color:#5F5',
|
||||
'eb11': 'background-color:#FF5',
|
||||
'eb12': 'background-color:#55F',
|
||||
'eb13': 'background-color:#F5F',
|
||||
'eb14': 'background-color:#5FF',
|
||||
'eb15': 'background-color:#FFF'
|
||||
};
|
||||
|
||||
// Define the default styles.
|
||||
var DEFAULTS = {
|
||||
fg: '#FFF',
|
||||
bg: '#000'
|
||||
};
|
||||
|
||||
var __slice = [].slice;
|
||||
|
||||
var toHexString = function(num) {
|
||||
num = num.toString(16);
|
||||
while (num.length < 2) {
|
||||
num = "0" + num;
|
||||
}
|
||||
return num;
|
||||
};
|
||||
|
||||
var escapeHtml = function (unsafe) {
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
};
|
||||
|
||||
// Define the derived styles.
|
||||
[0, 1, 2, 3, 4, 5].forEach(
|
||||
function(red) {
|
||||
return [0, 1, 2, 3, 4, 5].forEach(
|
||||
function(green) {
|
||||
return [0, 1, 2, 3, 4, 5].forEach(
|
||||
function(blue) {
|
||||
var b, c, g, n, r, rgb;
|
||||
c = 16 + (red * 36) + (green * 6) + blue;
|
||||
r = red > 0 ? red * 40 + 55 : 0;
|
||||
g = green > 0 ? green * 40 + 55 : 0;
|
||||
b = blue > 0 ? blue * 40 + 55 : 0;
|
||||
rgb = ((function() {
|
||||
var _i, _len, _ref, _results;
|
||||
_ref = [r, g, b];
|
||||
_results = [];
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
n = _ref[_i];
|
||||
_results.push(toHexString(n));
|
||||
}
|
||||
return _results;
|
||||
})()).join('');
|
||||
STYLES["ef" + c] = "color:#" + rgb;
|
||||
return STYLES["eb" + c] = "background-color:#" + rgb;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
(function() {
|
||||
_results = [];
|
||||
for (_i = 0; _i <= 23; _i++){ _results.push(_i); }
|
||||
return _results;
|
||||
}).apply(this).forEach(
|
||||
function(gray) {
|
||||
var c, l;
|
||||
c = gray + 232;
|
||||
l = toHexString(gray * 10 + 8);
|
||||
STYLES["ef" + c] = "color:#" + l + l + l;
|
||||
return STYLES["eb" + c] = "background-color:#" + l + l + l;
|
||||
});
|
||||
|
||||
// Define the filter class which will track all the ANSI style state.
|
||||
function Filter(options) {
|
||||
this.opts = $.extend(true, DEFAULTS, options || {});
|
||||
this.input = [];
|
||||
this.stack = [];
|
||||
}
|
||||
|
||||
Filter.create = function() {
|
||||
return new Filter();
|
||||
};
|
||||
|
||||
Filter.prototype.toHtml = function(input) {
|
||||
this.resetStyles();
|
||||
var html = this.addInputToStream(input) + this.getTeardownHtml();
|
||||
this.resetStyles();
|
||||
return html;
|
||||
};
|
||||
|
||||
Filter.prototype.addInputToStream = function(input) {
|
||||
var buf = [];
|
||||
|
||||
this.input = typeof input === 'string' ? [input] : input;
|
||||
this.forEach(function(chunk) {
|
||||
return buf.push(chunk);
|
||||
});
|
||||
|
||||
return buf.join('');
|
||||
};
|
||||
|
||||
Filter.prototype.getSetupHtml = function() {
|
||||
return this.stack.map(function(data) {
|
||||
return data['html'];
|
||||
}).join('');
|
||||
};
|
||||
|
||||
Filter.prototype.getTeardownHtml = function() {
|
||||
var stackCopy = this.stack.slice();
|
||||
return stackCopy.reverse().map(function(data) {
|
||||
return "</" + data['kind'] + ">";
|
||||
}).join('');
|
||||
};
|
||||
|
||||
Filter.prototype.forEach = function(callback) {
|
||||
var that = this;
|
||||
var buf = '';
|
||||
var handleDisplay = function(code) {
|
||||
code = parseInt(code, 10);
|
||||
if (code === -1) {
|
||||
callback('<br/>');
|
||||
}
|
||||
if (code === 0) {
|
||||
callback(that.getTeardownHtml());
|
||||
that.resetStyles();
|
||||
}
|
||||
if (code === 1) {
|
||||
callback(that.pushTag('b'));
|
||||
}
|
||||
if (code === 2) {
|
||||
|
||||
}
|
||||
if ((2 < code && code < 5)) {
|
||||
callback(that.pushTag('u'));
|
||||
}
|
||||
if ((4 < code && code < 7)) {
|
||||
callback(that.pushTag('blink'));
|
||||
}
|
||||
if (code === 7) {
|
||||
|
||||
}
|
||||
if (code === 8) {
|
||||
callback(that.pushStyle('display:none'));
|
||||
}
|
||||
if (code === 9) {
|
||||
callback(that.pushTag('strike'));
|
||||
}
|
||||
if (code === 24) {
|
||||
callback(that.closeTag('u'));
|
||||
}
|
||||
if ((29 < code && code < 38)) {
|
||||
callback(that.pushStyle("ef" + (code - 30)));
|
||||
}
|
||||
if (code === 39) {
|
||||
callback(that.pushStyle("color:" + that.opts.fg));
|
||||
}
|
||||
if ((39 < code && code < 48)) {
|
||||
callback(that.pushStyle("eb" + (code - 40)));
|
||||
}
|
||||
if (code === 49) {
|
||||
callback(that.pushStyle("background-color:" + that.opts.bg));
|
||||
}
|
||||
if ((89 < code && code < 98)) {
|
||||
callback(that.pushStyle("ef" + (8 + (code - 90))));
|
||||
}
|
||||
if ((99 < code && code < 108)) {
|
||||
return callback(that.pushStyle("eb" + (8 + (code - 100))));
|
||||
}
|
||||
};
|
||||
|
||||
this.input.forEach(function(chunk) {
|
||||
buf += chunk;
|
||||
return that.tokenize(buf, function(tok, data) {
|
||||
switch (tok) {
|
||||
case 'text':
|
||||
return callback(escapeHtml(data));
|
||||
case 'display':
|
||||
return handleDisplay(data);
|
||||
case 'xterm256':
|
||||
return callback(that.pushStyle("ef" + data));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Filter.prototype.pushTag = function(tag, style) {
|
||||
if (style == null) {
|
||||
style = '';
|
||||
}
|
||||
if (style.length && style.indexOf(':') === -1) {
|
||||
style = STYLES[style];
|
||||
}
|
||||
var html = ["<" + tag, (style ? " style=\"" + style + "\"" : void 0), ">"].join('');
|
||||
this.stack.push({'html': html, 'kind': tag});
|
||||
return html;
|
||||
};
|
||||
|
||||
Filter.prototype.pushStyle = function(style) {
|
||||
return this.pushTag("span", style);
|
||||
};
|
||||
|
||||
Filter.prototype.closeTag = function(style) {
|
||||
var last;
|
||||
if (this.stack.slice(-1)[0]['kind'] === style) {
|
||||
last = this.stack.pop();
|
||||
}
|
||||
if (last != null) {
|
||||
return "</" + style + ">";
|
||||
}
|
||||
};
|
||||
|
||||
Filter.prototype.resetStyles = function() {
|
||||
this.stack = [];
|
||||
};
|
||||
|
||||
Filter.prototype.tokenize = function(text, callback) {
|
||||
var ansiHandler, ansiMatch, ansiMess, handler, i, length, newline, process, realText, remove, removeXterm256, tokens, _j, _len, _results1,
|
||||
_this = this;
|
||||
ansiMatch = false;
|
||||
ansiHandler = 3;
|
||||
remove = function(m) {
|
||||
return '';
|
||||
};
|
||||
removeXterm256 = function(m, g1) {
|
||||
callback('xterm256', g1);
|
||||
return '';
|
||||
};
|
||||
newline = function(m) {
|
||||
if (_this.opts.newline) {
|
||||
callback('display', -1);
|
||||
} else {
|
||||
callback('text', m);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
ansiMess = function(m, g1) {
|
||||
var code, _j, _len;
|
||||
ansiMatch = true;
|
||||
if (g1.trim().length === 0) {
|
||||
g1 = '0';
|
||||
}
|
||||
g1 = g1.trimRight(';').split(';');
|
||||
for (_j = 0, _len = g1.length; _j < _len; _j++) {
|
||||
code = g1[_j];
|
||||
callback('display', code);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
realText = function(m) {
|
||||
callback('text', m);
|
||||
return '';
|
||||
};
|
||||
tokens = [
|
||||
{
|
||||
pattern: /^\x08+/,
|
||||
sub: remove
|
||||
}, {
|
||||
pattern: /^\x1b\[38;5;(\d+)m/,
|
||||
sub: removeXterm256
|
||||
}, {
|
||||
pattern: /^\n+/,
|
||||
sub: newline
|
||||
}, {
|
||||
pattern: /^\x1b\[((?:\d{1,3};?)+|)m/,
|
||||
sub: ansiMess
|
||||
}, {
|
||||
pattern: /^\x1b\[?[\d;]{0,3}/,
|
||||
sub: remove
|
||||
}, {
|
||||
pattern: /^([^\x1b\x08\n]+)/,
|
||||
sub: realText
|
||||
}
|
||||
];
|
||||
process = function(handler, i) {
|
||||
var matches;
|
||||
if (i > ansiHandler && ansiMatch) {
|
||||
return;
|
||||
} else {
|
||||
ansiMatch = false;
|
||||
}
|
||||
matches = text.match(handler.pattern);
|
||||
text = text.replace(handler.pattern, handler.sub);
|
||||
};
|
||||
_results1 = [];
|
||||
while ((length = text.length) > 0) {
|
||||
for (i = _j = 0, _len = tokens.length; _j < _len; i = ++_j) {
|
||||
handler = tokens[i];
|
||||
process(handler, i);
|
||||
}
|
||||
if (text.length === length) {
|
||||
break;
|
||||
} else {
|
||||
_results1.push(void 0);
|
||||
}
|
||||
}
|
||||
return _results1;
|
||||
};
|
||||
|
||||
return Filter;
|
||||
})());
|
|
@ -12,16 +12,7 @@
|
|||
<div class="quay-spinner"></div>
|
||||
</div>
|
||||
|
||||
<div class="container" ng-show="!user.anonymous && uploading">
|
||||
<span class="message">Uploading file {{ upload_file }}</span>
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar" role="progressbar" aria-valuenow="{{ upload_progress }}" aria-valuemin="0" aria-valuemax="100" style="{{ 'width: ' + upload_progress + '%' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="container new-repo" ng-show="!user.anonymous && !creating && !uploading && !building">
|
||||
<div class="container new-repo" ng-show="!user.anonymous && !creating && !building">
|
||||
<form method="post" name="newRepoForm" id="newRepoForm" ng-submit="createNewRepo()">
|
||||
|
||||
<!-- Header -->
|
||||
|
@ -109,11 +100,9 @@
|
|||
</div>
|
||||
|
||||
<div class="initialize-repo" ng-show="repo.initialize">
|
||||
<div class="init-description">
|
||||
Upload a Dockerfile or a zip file containing a Dockerfile <b>in the root directory</b>
|
||||
</div>
|
||||
|
||||
<input id="file-drop" class="file-drop" type="file">
|
||||
<div class="dockerfile-build-form" repository="createdForBuild" upload-failed="handleBuildFailed(message)"
|
||||
build-started="handleBuildStarted()" build-failed="handleBuildFailed(message)" start-now="createdForBuild"
|
||||
has-dockerfile="hasDockerfile" uploading="uploading" building="building"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -123,7 +112,7 @@
|
|||
<div class="col-md-1"></div>
|
||||
<div class="col-md-8">
|
||||
<button class="btn btn-large btn-success" type="submit"
|
||||
ng-disabled="newRepoForm.$invalid || (repo.is_public == '0' && (planRequired || checkingPlan))">
|
||||
ng-disabled="uploading || building || newRepoForm.$invalid || (repo.is_public == '0' && (planRequired || checkingPlan)) || (repo.initialize && !hasDockerfile)">
|
||||
Create Repository
|
||||
</button>
|
||||
</div>
|
||||
|
@ -157,26 +146,6 @@
|
|||
</div><!-- /.modal -->
|
||||
|
||||
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="missingfileModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">File required</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
A file is required in order to initialize a repository.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="cannotcreateModal">
|
||||
<div class="modal-dialog">
|
||||
|
@ -195,26 +164,6 @@
|
|||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="couldnotbuildModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">Cannot initialize repository</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
The repository could not be initialized with the selected Dockerfile. Please try again later.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="couldnotsubscribeModal">
|
||||
<div class="modal-dialog">
|
||||
|
|
|
@ -9,6 +9,13 @@
|
|||
<span class="repo-circle no-background" repo="repo"></span>
|
||||
<span class="repo-breadcrumb" repo="repo" subsection-icon="'fa-tasks'" subsection="'Build History'"></span>
|
||||
</h3>
|
||||
|
||||
<div class="repo-controls">
|
||||
<button class="btn btn-success" ng-click="showNewBuildDialog()">
|
||||
<i class="fa fa-plus"></i>
|
||||
New Dockerfile Build
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="!builds.length">
|
||||
|
@ -19,7 +26,7 @@
|
|||
<!-- 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">
|
||||
<li ng-class="currentBuild == build ? 'active' : ''" ng-repeat="build in builds | reverse">
|
||||
<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>
|
||||
|
@ -67,17 +74,47 @@
|
|||
<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>
|
||||
<span class="message" bo-html="processANSI(entry.message, container)"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="margin-top: 10px">
|
||||
<span class="quay-spinner" ng-show="polling"></span>
|
||||
<button class="btn" ng-show="(build.phase == 'error' || build.phase == 'complete') && build.resource_key"
|
||||
ng-class="build.phase == 'error' ? 'btn-success' : 'btn-default'"
|
||||
ng-click="askRestartBuild(build)">
|
||||
<i class="fa fa-refresh"></i>
|
||||
Run Build Again
|
||||
</button>
|
||||
<span class="build-id">{{ build.id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dockerfile-build-dialog" show-now="buildDialogShowCounter" repository="repo"
|
||||
build-started="handleBuildStarted(build)">
|
||||
</div>
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="confirmRestartBuildModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">Run Dockerfile Build?</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Are you sure you want to run this Dockerfile build again? The results will be immediately pushed to the repository.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" ng-click="restartBuild(currentBuild)">Run Build</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
</div>
|
||||
|
|
|
@ -5,51 +5,65 @@
|
|||
</div>
|
||||
|
||||
<div class="resource-view" resource="repository" error-message="'No Repository Found'">
|
||||
<div class="container repo">
|
||||
<div class="container repo repo-view">
|
||||
<!-- Repo Header -->
|
||||
<div class="header">
|
||||
<h3>
|
||||
<span class="repo-circle" repo="repo"></span>
|
||||
<span class="repo-breadcrumb" repo="repo"></span>
|
||||
<span class="settings-cog" ng-show="repo.can_admin" title="Repository Settings" bs-tooltip="tooltip.title" data-placement="bottom">
|
||||
<a id="admin-cog" href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/admin' }}">
|
||||
<i class="fa fa-cog fa-lg"></i>
|
||||
</a>
|
||||
</span>
|
||||
<span class="repo-breadcrumb" repo="repo"></span>
|
||||
</h3>
|
||||
|
||||
<!-- Pull command -->
|
||||
<div class="pull-command visible-md visible-lg" style="display: none;">
|
||||
<span class="pull-command-title">Pull repository:</span>
|
||||
<div class="pull-container">
|
||||
<div class="input-group">
|
||||
<input id="pull-text" type="text" class="form-control" value="{{ 'docker pull quay.io/' + repo.namespace + '/' + repo.name }}" readonly>
|
||||
<span id="copyClipboard" class="input-group-addon" title="Copy to Clipboard" data-clipboard-target="pull-text">
|
||||
<i class="fa fa-copy"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="clipboardCopied" class="hovering" style="display: none">
|
||||
Copied to clipboard
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status boxes -->
|
||||
<div class="status-boxes">
|
||||
<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">
|
||||
<div class="repo-controls">
|
||||
<!-- Builds -->
|
||||
<div class="dropdown" data-placement="top" style="display: inline-block"
|
||||
bs-tooltip="buildsInfo ? 'Dockerfile Builds Running: ' + (buildsInfo.length) : 'Dockerfile Build'"
|
||||
ng-show="repo.can_write || buildsInfo.length">
|
||||
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-tasks fa-lg"></i>
|
||||
<span class="count" ng-class="buildsInfo ? 'visible' : ''"><span>{{ buildsInfo ? buildsInfo.length : '' }}</span></span>
|
||||
<b class="caret"></b>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-show="repo.can_write"><a href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/build' }}">
|
||||
<i class="fa fa-tasks"></i>Dockerfile Build History</a>
|
||||
</li>
|
||||
<li ng-show="repo.can_write">
|
||||
<a href="javascript:void(0)" ng-click="showNewBuildDialog()">
|
||||
<i class="fa fa-plus" style="margin-left: 1px; margin-right: 8px;"></i>New Dockerfile Build
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation" class="divider" ng-show="buildsInfo && repo.can_write"></li>
|
||||
<li role="presentation" class="dropdown-header" ng-show="buildsInfo">Current Builds</li>
|
||||
<li ng-repeat="buildInfo in buildsInfo">
|
||||
<div class="build-info" ng-class="repo.can_write ? 'clickable' : ''" ng-click="showBuild(buildInfo)">
|
||||
<div class="build-info" ng-class="repo.can_write ? 'clickable' : ''" ng-click="repo.can_write && showBuild(buildInfo)">
|
||||
<span class="build-status" build="buildInfo"></span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Admin -->
|
||||
<a id="admin-cog" href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/admin' }}"
|
||||
ng-show="repo.can_admin">
|
||||
<button class="btn btn-default" title="Repository Settings" bs-tooltip="tooltip.title" data-placement="top">
|
||||
<i class="fa fa-cog fa-lg"></i></button></a>
|
||||
|
||||
<!-- Pull Command -->
|
||||
<span class="pull-command visible-md-inline">
|
||||
<div class="pull-container" title="Pull repository" bs-tooltip="tooltip.title">
|
||||
<div class="input-group">
|
||||
<input id="pull-text" type="text" class="form-control" value="{{ 'docker pull quay.io/' + repo.namespace + '/' + repo.name }}" readonly>
|
||||
<span id="copyClipboard" class="input-group-addon" title="Copy to Clipboard" data-clipboard-target="pull-text">
|
||||
<i class="fa fa-copy"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="clipboardCopied" class="hovering" style="display: none">
|
||||
Copied to clipboard
|
||||
</div>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -72,7 +86,7 @@ sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
|
|||
|
||||
<div class="repo-content" ng-show="!currentTag.image && repo.is_building">
|
||||
<div class="empty-message">
|
||||
Your build is currently processing, if this takes longer than an hour, please contact <a href="mailto:support@quay.io">Quay.io support</a>
|
||||
A build is currently processing. If this takes longer than an hour, please <a href="/contact">contact us</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -235,6 +249,10 @@ sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dockerfile-build-dialog" show-now="buildDialogShowCounter" repository="repo"
|
||||
build-started="handleBuildStarted(build)">
|
||||
</div>
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="confirmdeleteTagModal">
|
||||
<div class="modal-dialog">
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
<script src="static/lib/angulartics-google-analytics.js"></script>
|
||||
<script src="static/lib/angular-md5.js"></script>
|
||||
<script src="static/lib/bindonce.min.js"></script>
|
||||
<script src="static/lib/ansi2html.js"></script>
|
||||
|
||||
<script src="static/lib/angular-moment.min.js"></script>
|
||||
<script src="static/lib/angular-cookies.min.js"></script>
|
||||
|
|
|
@ -129,7 +129,12 @@ class TestBuildLogs(BuildLogs):
|
|||
|
||||
@staticmethod
|
||||
def _generate_logs(count):
|
||||
return [(1, {'message': get_sentence()}, None) for _ in range(count)]
|
||||
others = []
|
||||
if random.randint(0, 10) <= 8:
|
||||
count = count - 2
|
||||
others = [(1, {'message': '\x1b[91m' + get_sentence()}, None), (1, {'message': '\x1b[0m'}, None)]
|
||||
|
||||
return others + [(1, {'message': get_sentence()}, None) for _ in range(count)]
|
||||
|
||||
@staticmethod
|
||||
def _compute_total_completion(statuses, total_images):
|
||||
|
|
Reference in a new issue