Better build dialog UX

We now automatically validate the chosen Dockerfile/archive, and automatically check to see if a robot is needed
This commit is contained in:
Joseph Schorr 2015-08-19 16:15:21 -04:00
parent 4cb4288672
commit b3fcd3f84e
5 changed files with 234 additions and 37 deletions

View file

@ -15,4 +15,23 @@ angular.module('quay').directive("filePresent", [function () {
});
}
}
}]);
/**
* Raises the 'filesChanged' event on the scope if a file on the marked <input type="file"> exists.
*/
angular.module('quay').directive("filesChanged", [function () {
return {
restrict: 'A',
scope: {
'filesChanged': "&"
},
link: function (scope, element, attributes) {
element.bind("change", function (changeEvent) {
scope.$apply(function() {
scope.filesChanged({'files': changeEvent.target.files});
});
});
}
}
}]);

View file

@ -20,10 +20,66 @@ angular.module('quay').directive('dockerfileBuildForm', function () {
'uploading': '=uploading',
'building': '=building'
},
controller: function($scope, $element, ApiService) {
$scope.internal = {'hasDockerfile': false};
$scope.pull_entity = null;
$scope.is_public = true;
controller: function($scope, $element, ApiService, DockerfileService, Config) {
var MEGABYTE = 1000000;
var MAX_FILE_SIZE = 100 * MEGABYTE;
$scope.hasDockerFile = false;
$scope.pullEntity = null;
$scope.dockerfileState = 'none';
$scope.privateBaseRepository = null;
$scope.isReady = false;
$scope.handleFilesChanged = function(files) {
$scope.dockerfileError = '';
$scope.privateBaseRepository = null;
$scope.pullEntity = null;
$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);
} else {
$scope.dockerfileState = 'ready';
}
} else {
$scope.dockerfileState = 'none';
}
};
var handleBuildFailed = function(message) {
message = message || 'Dockerfile build failed to start';
@ -97,8 +153,8 @@ angular.module('quay').directive('dockerfileBuildForm', function () {
'file_id': fileId
};
if (!$scope.is_public && $scope.pull_entity) {
data['pull_robot'] = $scope.pull_entity['name'];
if ($scope.pullEntity) {
data['pull_robot'] = $scope.pullEntity['name'];
}
var params = {
@ -180,13 +236,28 @@ angular.module('quay').directive('dockerfileBuildForm', function () {
});
};
var checkIsReady = function() {
$scope.isReady = $scope.internal.hasDockerfile && ($scope.is_public || $scope.pull_entity);
var checkReady = function() {
$scope.isReady = ($scope.dockerfileState == 'ready' &&
(!$scope.privateBaseRepository || $scope.pullEntity));
};
$scope.$watch('pull_entity', checkIsReady);
$scope.$watch('is_public', checkIsReady);
$scope.$watch('internal.hasDockerfile', checkIsReady);
var checkEntity = function() {
$scope.currentRobotHasPermission = null;
if (!$scope.pullEntity) { return; }
var permParams = {
'repository': $scope.privateBaseRepository,
'username': $scope.pullEntity.name
};
ApiService.getUserTransitivePermission(null, permParams).then(function(resp) {
$scope.currentRobotHasPermission = resp['permissions'].length > 0;
});
};
$scope.$watch('pullEntity', checkEntity);
$scope.$watch('pullEntity', checkReady);
$scope.$watch('dockerfileState', checkReady);
$scope.$watch('startNow', function() {
if ($scope.startNow && $scope.repository && !$scope.uploading && !$scope.building) {

View file

@ -0,0 +1,98 @@
/**
* Service which provides helper methods for extracting information out from a Dockerfile
* or an archive containing a Dockerfile.
*/
angular.module('quay').factory('DockerfileService', ['DataFileService', 'Config', function(DataFileService, Config) {
var dockerfileService = {};
function DockerfileInfo(contents) {
this.contents = contents;
}
DockerfileInfo.prototype.getRegistryBaseImage = function() {
var baseImage = this.getBaseImage();
if (!baseImage) {
return;
}
if (baseImage.indexOf(Config.getDomain() + '/') != 0) {
return;
}
return baseImage.substring(Config.getDomain().length + 1);
};
DockerfileInfo.prototype.getBaseImage = function() {
var fromIndex = this.contents.indexOf('FROM ');
if (fromIndex < 0) {
return;
}
var newline = this.contents.indexOf('\n', fromIndex);
if (newline < 0) {
newline = this.contents.length;
}
return $.trim(this.contents.substring(fromIndex + 'FROM '.length, newline));
};
DockerfileInfo.forData = function(contents) {
if (contents.indexOf('FROM ') < 0) {
return;
}
return new DockerfileInfo(contents);
};
var processFiles = function(files, dataArray, success, failure) {
// The files array will be empty if the submitted file was not an archive. We therefore
// treat it as a single Dockerfile.
if (files.length == 0) {
DataFileService.arrayToString(dataArray, function(c) {
var result = DockerfileInfo.forData(c);
if (!result) {
failure('File chosen is not a valid Dockerfile');
return;
}
success(result);
});
return;
}
var found = false;
files.forEach(function(file) {
if (file['name'] == 'Dockerfile') {
DataFileService.blobToString(file.toBlob(), function(c) {
var result = DockerfileInfo.forData(c);
if (!result) {
failure('Dockerfile inside archive is not a valid Dockerfile');
return;
}
success(result);
});
found = true;
}
});
if (!found) {
failure('No Dockerfile found in root of archive');
}
};
dockerfileService.getDockerfile = function(file, success, failure) {
var reader = new FileReader();
reader.onload = function(e) {
var dataArray = reader.result;
DataFileService.readDataArrayAsPossibleArchive(dataArray, function(files) {
processFiles(files, dataArray, success, failure);
}, failure);
};
reader.onerror = failure;
reader.readAsArrayBuffer(file);
};
return dockerfileService;
}]);