Merge pull request #381 from coreos-inc/builddialog

Better build dialog UX
This commit is contained in:
josephschorr 2015-08-20 14:32:26 -04:00
commit 9098e0a1fe
5 changed files with 234 additions and 37 deletions

View file

@ -3,6 +3,10 @@
white-space: nowrap;
}
.dockerfile-build-form .file-drop {
padding: 0px;
}
.dockerfile-build-form input[type="file"] {
margin: 0px;
}
@ -10,6 +14,11 @@
.dockerfile-build-form .help-text {
font-size: 13px;
color: #aaa;
margin-bottom: 20px;
padding-left: 22px;
margin-top: 10px;
margin-bottom: 16px;
}
.dockerfile-build-form dd {
padding-left: 20px;
padding-top: 14px;
}

View file

@ -12,35 +12,35 @@
<dl>
<dt>Dockerfile or <code>.tar.gz</code> or <code>.zip</code>:</dt>
<dd>
<input id="file-drop" class="file-drop" type="file" file-present="internal.hasDockerfile">
<div class="help-text">If an archive, the Dockerfile must be at the root</div>
<div class="co-alert co-alert-danger" ng-if="dockerfileState == 'error'">
{{ dockerfileError }}
</div>
<input id="file-drop" class="file-drop" type="file" files-changed="handleFilesChanged(files)">
<div class="help-text">Note: If an archive, the Dockerfile must be in the root directory.</div>
<div ng-if="dockerfileState == 'loading'">
Reading Dockerfile: <span class="cor-loader-inline"></span>
</div>
</dd>
</dl>
<dl>
<dl ng-show="privateBaseRepository">
<dt>Base Image Pull Credentials:</dt>
<dd style="margin: 20px;">
<!-- Select credentials -->
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-default"
ng-class="is_public ? 'active btn-info' : ''"
ng-click="is_public = true">
None
</button>
<button type="button" class="btn btn-default"
ng-class="is_public ? '' : 'active btn-info'"
ng-click="is_public = false">
<i class="fa ci-robot"></i>
Robot account
</button>
<dd>
<div class="co-alert co-alert-warning"
ng-if="currentRobotHasPermission === false">
Warning: Robot account <strong>{{ pullEntity.name }}</strong> does not have
read permission on repository <strong>{{ privateBaseRepository }}</strong>, so
this build will fail with an authorization error.
</div>
<!-- Robot Select -->
<div ng-show="!is_public" style="margin-top: 10px">
<div class="entity-search" namespace="repository.namespace"
placeholder="'Select robot account for pulling...'"
current-entity="pull_entity"
allowed-entities="['robot']"></div>
<div class="entity-search" namespace="repository.namespace"
placeholder="'Select robot account for pulling'"
current-entity="pullEntity"
allowed-entities="['robot']"></div>
<div class="help-text">
The selected Dockerfile contains a <code>FROM</code> that refers to the private
<span class="registry-name"></span> repository <strong>{{ privateBaseRepository }}</strong>.
A robot account with read access to that repository is required for the build.
</div>
</dd>
</dl>

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;
}]);