Merge pull request #381 from coreos-inc/builddialog
Better build dialog UX
This commit is contained in:
commit
9098e0a1fe
5 changed files with 234 additions and 37 deletions
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}]);
|
|
@ -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) {
|
||||
|
|
98
static/js/services/dockerfile-service.js
Normal file
98
static/js/services/dockerfile-service.js
Normal 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;
|
||||
}]);
|
Reference in a new issue