Redo the UI for the run trigger dialog to be much cleaner

Fixes #774
This commit is contained in:
Joseph Schorr 2016-09-29 15:19:21 +02:00
parent edc2bc8b93
commit e85a1bce0a
13 changed files with 541 additions and 369 deletions

View file

@ -14,30 +14,12 @@ angular.module('quay').directive('dockerfileBuildDialog', function () {
'buildStarted': '&buildStarted'
},
controller: function($scope, $element, ApiService) {
$scope.building = false;
$scope.uploading = false;
$scope.startCounter = 0;
$scope.viewTriggers = false;
$scope.triggers = null;
$scope.startTriggerCounter = 0;
$scope.startTrigger = null;
$scope.handleBuildStarted = function(build) {
$element.find('.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.showTriggers = function(value) {
$scope.viewTriggers = value;
};
@ -48,11 +30,27 @@ angular.module('quay').directive('dockerfileBuildDialog', function () {
$scope.startTriggerCounter++;
};
$scope.startBuild = function() {
$scope.buildStarting = true;
$scope.startBuildCallback(function(status, messageOrBuild) {
$element.find('.dockerfilebuildModal').modal('hide');
if (status) {
$scope.buildStarted({'build': messageOrBuild});
} else {
bootbox.alert(messageOrBuild || 'Could not start build');
}
});
};
$scope.readyForBuild = function(startBuild) {
$scope.startBuildCallback = startBuild;
};
$scope.$watch('showNow', function(sn) {
if (sn && $scope.repository) {
$scope.viewTriggers = false;
$scope.startTrigger = null;
$scope.errorMessage = null;
$scope.buildStarting = false;
$element.find('.dockerfilebuildModal').modal({});

View file

@ -10,151 +10,69 @@ angular.module('quay').directive('dockerfileBuildForm', function () {
restrict: 'C',
scope: {
'repository': '=repository',
'startNow': '=startNow',
'isReady': '=isReady',
'uploadFailed': '&uploadFailed',
'uploadStarted': '&uploadStarted',
'buildStarted': '&buildStarted',
'buildFailed': '&buildFailed',
'missingFile': '&missingFile',
'uploading': '=uploading',
'building': '=building'
'isReady': '=?isReady',
'readyForBuild': '&readyForBuild'
},
controller: function($scope, $element, ApiService, DockerfileService, Config) {
var MEGABYTE = 1000000;
var MAX_FILE_SIZE = 100 * MEGABYTE;
$scope.state = 'empty';
var resetState = function() {
$scope.hasDockerFile = false;
$scope.pullEntity = null;
$scope.dockerfileState = 'none';
$scope.privateBaseRepository = null;
$scope.isReady = false;
var checkPrivateImage = function(baseImage) {
var params = {
'repository': baseImage
};
$scope.state = 'checking-image';
ApiService.getRepo(null, params).then(function(repository) {
$scope.privateBaseRepository = repository.is_public ? null : baseImage;
$scope.state = 'awaiting-bot';
}, function() {
$scope.privateBaseRepository = baseImage;
$scope.state = 'awaiting-bot';
});
};
resetState();
$scope.handleFilesChanged = function(files) {
$scope.dockerfileError = '';
$scope.privateBaseRepository = null;
$scope.handleFilesSelected = function(files, opt_callback) {
$scope.pullEntity = null;
$scope.state = 'checking';
$scope.selectedFiles = files;
$scope.dockerfileState = 'loading';
$scope.hasDockerFile = files.length > 0;
var checkPrivateImage = function(baseImage) {
var params = {
'repository': baseImage
};
ApiService.getRepo(null, params).then(function(repository) {
$scope.privateBaseRepository = repository.is_public ? null : baseImage;
$scope.dockerfileState = 'ready';
}, function() {
$scope.privateBaseRepository = baseImage;
$scope.dockerfileState = 'ready';
});
};
var loadError = function(msg) {
$scope.$apply(function() {
$scope.dockerfileError = msg || 'Could not read uploaded Dockerfile';
$scope.dockerfileState = 'error';
});
};
var gotDockerfile = function(df) {
$scope.$apply(function() {
var baseImage = df.getRegistryBaseImage();
if (baseImage) {
checkPrivateImage(baseImage);
} else {
$scope.dockerfileState = 'ready';
}
});
};
if (files.length > 0) {
if (files[0].size < MAX_FILE_SIZE) {
DockerfileService.getDockerfile(files[0], gotDockerfile, loadError);
DockerfileService.getDockerfile(files[0], function(df) {
var baseImage = df.getRegistryBaseImage();
if (baseImage) {
checkPrivateImage(baseImage);
} else {
$scope.dockerfileState = 'ready';
$scope.state = 'ready';
}
} else {
$scope.dockerfileState = 'none';
}
};
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"
}
}
$scope.$apply(function() {
opt_callback && opt_callback(true, 'Dockerfile found and valid')
});
}
};
}, function(msg) {
$scope.state = 'empty';
$scope.privateBaseRepository = null;
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"
}
}
$scope.$apply(function() {
opt_callback && opt_callback(false, msg || 'Could not find valid Dockerfile');
});
}
});
};
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"
}
}
});
}
$scope.handleFilesCleared = function() {
$scope.state = 'empty';
$scope.pullEntity = null;
$scope.privateBaseRepository = null;
};
var startBuild = function(fileId) {
$scope.building = true;
$scope.handleFilesValidated = function(uploadFiles) {
$scope.uploadFilesCallback = uploadFiles;
};
var requestRepoBuild = function(buildPackId, opt_callback) {
var repo = $scope.repository;
var data = {
'file_id': fileId
'file_id': buildPackId
};
if ($scope.pullEntity) {
@ -166,101 +84,25 @@ angular.module('quay').directive('dockerfileBuildForm', function () {
};
ApiService.requestRepoBuild(data, params).then(function(resp) {
$scope.building = false;
$scope.uploading = false;
if ($scope.buildStarted) {
$scope.buildStarted({'build': resp});
}
opt_callback && opt_callback(true, resp);
}, function(resp) {
$scope.building = false;
$scope.uploading = false;
handleBuildFailed(resp.message);
opt_callback && opt_callback(false, 'Could not start build');
$scope.handleFilesSelected($scope.selectedFiles);
});
};
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;
var status = request.status;
if (state == 4) {
if (Math.floor(status / 100) == 2) {
$scope.$apply(function() {
startBuild(fileId);
$scope.uploading = false;
});
} else {
var message = request.statusText;
if (status == 413) {
message = 'Selected file too large to upload';
}
$scope.$apply(function() {
handleUploadFailed(message);
});
}
}
};
request.send(file);
};
var startFileUpload = function(repo) {
$scope.uploading = true;
$scope.uploading_progress = 0;
var uploader = $element.find('#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');
var startBuild = function(opt_callback) {
$scope.state = 'uploading-files';
$scope.uploadFilesCallback(function(status, messageOrIds) {
$scope.state = 'starting-build';
requestRepoBuild(messageOrIds[0], opt_callback);
});
};
var checkReady = function() {
$scope.isReady = ($scope.dockerfileState == 'ready' &&
(!$scope.privateBaseRepository || $scope.pullEntity));
};
var checkEntity = function() {
if (!$scope.pullEntity) { return; }
$scope.state = 'checking-bot';
$scope.currentRobotHasPermission = null;
if (!$scope.pullEntity) { return; }
@ -271,18 +113,17 @@ angular.module('quay').directive('dockerfileBuildForm', function () {
ApiService.getUserTransitivePermission(null, permParams).then(function(resp) {
$scope.currentRobotHasPermission = resp['permissions'].length > 0;
$scope.state = $scope.currentRobotHasPermission ? 'ready' : 'perm-error';
});
};
$scope.$watch('pullEntity', checkEntity);
$scope.$watch('pullEntity', checkReady);
$scope.$watch('dockerfileState', checkReady);
$scope.$watch('repository', resetState);
$scope.$watch('startNow', function() {
if ($scope.startNow && $scope.repository && !$scope.uploading && !$scope.building) {
startFileUpload();
$scope.$watch('state', function(state) {
$scope.isReady = state == 'ready';
if ($scope.isReady) {
$scope.readyForBuild({
'startBuild': startBuild
});
}
});
}

View file

@ -0,0 +1,154 @@
/**
* An element which adds a stylize box for uploading a file.
*/
angular.module('quay').directive('fileUploadBox', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/file-upload-box.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'allowMultiple': '@allowMultiple',
'selectMessage': '@selectMessage',
'filesSelected': '&filesSelected',
'filesCleared': '&filesCleared',
'filesValidated': '&filesValidated'
},
controller: function($rootScope, $scope, $element, ApiService) {
var MEGABYTE = 1000000;
var MAX_FILE_SIZE = MAX_FILE_SIZE_MB * MEGABYTE;
var MAX_FILE_SIZE_MB = 100;
var number = $rootScope.__fileUploadBoxIdCounter || 0;
$rootScope.__fileUploadBoxIdCounter = number + 1;
$scope.boxId = number;
$scope.state = 'clear';
$scope.selectedFiles = [];
var conductUpload = function(file, url, fileId, mimeType, progressCb, doneCb) {
var request = new XMLHttpRequest();
request.open('PUT', url, true);
request.setRequestHeader('Content-Type', mimeType);
request.onprogress = function(e) {
$scope.$apply(function() {
if (e.lengthComputable) { progressCb((e.loaded / e.total) * 100); }
});
};
request.onerror = function() {
$scope.$apply(function() { doneCb(false, 'Error when uploading'); });
};
request.onreadystatechange = function() {
var state = request.readyState;
var status = request.status;
if (state == 4) {
if (Math.floor(status / 100) == 2) {
$scope.$apply(function() { doneCb(true, fileId); });
} else {
var message = request.statusText;
if (status == 413) {
message = 'Selected file too large to upload';
}
$scope.$apply(function() { doneCb(false, message); });
}
}
};
request.send(file);
};
var uploadFiles = function(callback) {
var currentIndex = 0;
var fileIds = [];
var progressCb = function(progress) {
$scope.uploadProgress = progress;
};
var doneCb = function(status, messageOrId) {
if (status) {
fileIds.push(messageOrId);
currentIndex++;
performFileUpload();
} else {
callback(false, messageOrId);
}
};
var performFileUpload = function() {
// If we have finished uploading all of the files, invoke the overall callback
// with the list of file IDs.
if (currentIndex >= $scope.selectedFiles.length) {
callback(true, fileIds);
return;
}
// For the current file, retrieve a file-drop URL from the API for the file.
var currentFile = $scope.selectedFiles[currentIndex];
var mimeType = currentFile.type || 'application/octet-stream';
var data = {
'mimeType': mimeType
};
$scope.currentlyUploadingFile = currentFile;
$scope.uploadProgress = 0;
ApiService.getFiledropUrl(data).then(function(resp) {
// Perform the upload.
conductUpload(currentFile, resp.url, resp.file_id, mimeType, progressCb, doneCb);
}, function() {
callback(false, 'Could not retrieve upload URL');
});
};
// Start the uploading.
$scope.state = 'uploading';
performFileUpload();
};
$scope.handleFilesChanged = function(files) {
if ($scope.state == 'uploading') { return; }
$scope.message = null;
$scope.selectedFiles = files;
if (files.length == 0) {
$scope.state = 'clear';
$scope.filesCleared();
} else {
for (var i = 0; i < files.length; ++i) {
if (files[i].size > MAX_FILE_SIZE) {
$scope.state = 'error';
$scope.message = 'File ' + files[i].name + ' is larger than the maximum file ' +
'size of ' + MAX_FILE_SIZE_MB + ' MB';
return;
}
}
$scope.state = 'checking';
$scope.filesSelected({
'files': files,
'callback': function(status, message) {
$scope.state = status ? 'okay' : 'error';
$scope.message = message;
if (status) {
$scope.filesValidated({
'files': files,
'uploadFiles': uploadFiles
});
}
}
});
}
};
}
};
return directiveDefinitionObject;
});

View file

@ -27,37 +27,26 @@
$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.$watch('repo.name', function() {
$scope.createError = null;
});
$scope.startBuild = function() {
$scope.buildStarting = true;
$scope.startBuildCallback(function(status, messageOrBuild) {
if (status) {
$location.url('/repository/' + $scope.created.namespace + '/' + $scope.created.name +
'?tab=builds');
} else {
bootbox.alert(messageOrBuild || 'Could not start build');
}
});
};
$scope.readyForBuild = function(startBuild) {
$scope.startBuildCallback = startBuild;
};
$scope.createNewRepo = function() {
$scope.creating = true;
var repo = $scope.repo;
@ -72,9 +61,10 @@
$scope.creating = false;
$scope.created = created;
// Start the upload process if applicable.
// Start the build if applicable.
if ($scope.repo.initialize == 'dockerfile' || $scope.repo.initialize == 'zipfile') {
$scope.createdForBuild = created;
$scope.startBuild();
return;
}