Merge pull request #1895 from coreos-inc/better-dockerfile-dialog
Redo the UI for the run trigger dialog to be much cleaner
This commit is contained in:
commit
47e4b1b500
13 changed files with 541 additions and 369 deletions
|
@ -1659,4 +1659,40 @@ a:focus {
|
|||
.cor-confirm-dialog-element .progress-message {
|
||||
margin-bottom: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.co-top-tab-bar {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
padding-left: 10px;
|
||||
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.co-top-tab-bar li {
|
||||
display: inline-block;
|
||||
list-style: none;
|
||||
text-align: center;
|
||||
padding: 6px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
|
||||
bottom: -2px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.co-top-tab-bar li.active {
|
||||
color: #51a3d9;
|
||||
border-bottom: 2px solid #51a3d9;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.modal-header.ahead-of-tabs {
|
||||
border-bottom: 0px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
|
|
@ -20,3 +20,7 @@
|
|||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dockerfile-build-dialog-element .fa-exclamation-triangle {
|
||||
margin-right: 4px;
|
||||
color: #FCA657;
|
||||
}
|
|
@ -1,24 +1,20 @@
|
|||
.dockerfile-build-form table td {
|
||||
vertical-align: top;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dockerfile-build-form .file-drop {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.dockerfile-build-form input[type="file"] {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.dockerfile-build-form .help-text {
|
||||
font-size: 13px;
|
||||
color: #aaa;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.dockerfile-build-form dd {
|
||||
padding-left: 20px;
|
||||
padding-top: 14px;
|
||||
}
|
||||
.dockerfile-build-form .robot-permission {
|
||||
margin-top: 10px;
|
||||
background: #fafafa;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dockerfile-build-form .co-alert {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.dockerfile-build-form .starting-build {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
|
117
static/css/directives/ui/file-upload-box.css
Normal file
117
static/css/directives/ui/file-upload-box.css
Normal file
|
@ -0,0 +1,117 @@
|
|||
/* Based off of http://tympanus.net/Tutorials/CustomFileInputs/ */
|
||||
|
||||
.file-upload-box-element .file-drop {
|
||||
width: 0.1px;
|
||||
height: 0.1px;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.file-upload-box-element .file-input-container {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.file-upload-box-element .fa-times-circle {
|
||||
margin-right: 4px;
|
||||
color: #D64456;
|
||||
}
|
||||
|
||||
.file-upload-box-element .fa-check-circle {
|
||||
margin-right: 4px;
|
||||
color: #2FC98E;
|
||||
}
|
||||
|
||||
.file-upload-box-element .select-message {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.file-upload-box-element .file-drop + label {
|
||||
margin-top: 14px;
|
||||
margin-bottom: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-upload-box-element .file-drop + label .chosen-file {
|
||||
background: white;
|
||||
display: inline-block;
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
font-weight: normal;
|
||||
width: 250px;
|
||||
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
}
|
||||
|
||||
.file-upload-box-element .file-drop + label .choose-button {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
padding: 8px;
|
||||
background-color: #51a3d9;
|
||||
color: white;
|
||||
font-weight: normal;
|
||||
font-size: 16px;
|
||||
height: 34px;
|
||||
vertical-align: top;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
line-height: 18px;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.file-upload-box-element .file-drop + label:hover .choose-button {
|
||||
background-color: #3d769c;
|
||||
}
|
||||
|
||||
.file-upload-box-element .file-drop + label:hover .chosen-file {
|
||||
border-color: #3d769c;
|
||||
}
|
||||
|
||||
.file-upload-box-element .file-drop + label.okay .choose-button {
|
||||
background-color: #2FC98E;
|
||||
}
|
||||
|
||||
.file-upload-box-element .file-drop + label.okay .chosen-file {
|
||||
border-color: #2FC98E;
|
||||
}
|
||||
|
||||
.file-upload-box-element .file-drop + label.error .choose-button {
|
||||
background-color: #D64456;
|
||||
}
|
||||
|
||||
.file-upload-box-element .file-drop + label.error .chosen-file {
|
||||
border-color: #D64456;
|
||||
}
|
||||
|
||||
.file-upload-box-element .file-drop + label.uploading .choose-button {
|
||||
background-color: #666;
|
||||
}
|
||||
|
||||
.file-upload-box-element .file-drop + label.uploading .chosen-file {
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
|
||||
.file-upload-box-element .file-drop + label .fa {
|
||||
color: white;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.file-upload-box-element .file-drop + label * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.file-upload-box-element .status-message {
|
||||
font-size: 16px;
|
||||
}
|
|
@ -1,55 +1,68 @@
|
|||
<div class="dockerfile-build-dialog-element">
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade dockerfilebuildModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="co-dialog modal-dialog">
|
||||
<div class="modal-content" ng-show="triggersResource && triggersResource.loading">
|
||||
<div class="cor-loader"></div>
|
||||
</div>
|
||||
|
||||
<div class="modal-content" ng-show="!triggersResource || !triggersResource.loading">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<div class="modal-header ahead-of-tabs">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"
|
||||
ng-show="!buildStarting">×</button>
|
||||
<h4 class="modal-title">
|
||||
Start new Dockerfile build
|
||||
Start Repository Build
|
||||
</h4>
|
||||
</div>
|
||||
<ul class="co-top-tab-bar" ng-show="triggers.length > 0">
|
||||
<li class="co-top-tab" ng-class="viewTriggers ? 'active': ''" ng-click="showTriggers(true)">Invoke Build Trigger</li>
|
||||
<li class="co-top-tab" ng-class="!viewTriggers ? 'active': ''" ng-click="showTriggers(false)">Upload Dockerfile</li>
|
||||
</ul>
|
||||
<div class="modal-body">
|
||||
<div class="btn-group btn-group-sm" ng-show="triggers.length > 0">
|
||||
<button class="btn" ng-class="viewTriggers ? 'btn-default' : 'btn-info active'" ng-click="showTriggers(false)">
|
||||
<i class="fa fa-upload"></i>Upload Dockerfile
|
||||
</button>
|
||||
<button class="btn" ng-class="viewTriggers ? 'btn-info active' : 'btn-default'" ng-click="showTriggers(true)">
|
||||
<i class="fa fa-flash"></i>Start Build Trigger
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="co-alert co-alert-danger" ng-show="errorMessage">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<!-- Upload Dockerfile -->
|
||||
<div ng-show="!viewTriggers">
|
||||
<div class="dockerfile-build-form" repository="repository" upload-failed="handleBuildFailed(message)"
|
||||
build-started="handleBuildStarted(build)" build-failed="handleBuildFailed(message)" start-now="startCounter"
|
||||
is-ready="hasDockerfile" uploading="uploading" building="building"></div>
|
||||
<div class="dockerfile-build-form" repository="repository" is-ready="hasDockerfile"
|
||||
ready-for-build="readyForBuild(startBuild)"></div>
|
||||
</div>
|
||||
|
||||
<!-- Start Build Trigger -->
|
||||
<div ng-show="viewTriggers">
|
||||
<table class="trigger-list">
|
||||
<tr ng-repeat="trigger in triggers">
|
||||
<td><span class="trigger-description" trigger="trigger"></span></td>
|
||||
<td>
|
||||
<button class="btn btn-primary" ng-click="runTriggerNow(trigger)" ng-if="trigger.can_invoke">Run Trigger</button>
|
||||
<span class="empty" ng-if="!trigger.can_invoke">You do not have permission to run this trigger</span>
|
||||
</td>
|
||||
</tr>
|
||||
<p style="padding: 10px;">Manually running a build trigger provides the means for invoking a build trigger as-if
|
||||
called from the underlying service for the latest commit to a particular branch or tag.</p>
|
||||
|
||||
<table class="cor-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Trigger Description</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="trigger in triggers">
|
||||
<td><span class="trigger-description" trigger="trigger"></span></td>
|
||||
<td>
|
||||
<a href="javascript:void(0)" ng-click="runTriggerNow(trigger)"
|
||||
ng-if="trigger.can_invoke">Run Trigger Now</a>
|
||||
<span ng-if="!trigger.can_invoke"
|
||||
data-title="You do not have permission to run this trigger" bs-tooltip>
|
||||
<i class="fa fa-exclamation-triangle"></i> No permission to run
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" ng-click="startBuild()" ng-disabled="building || uploading || !hasDockerfile" ng-show="!viewTriggers">Start Build</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="startBuild()"
|
||||
ng-disabled="!hasDockerfile || buildStarting"
|
||||
ng-show="!viewTriggers">Start Build</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal"
|
||||
ng-disabled="buildStarting">Close</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
|
|
|
@ -1,48 +1,32 @@
|
|||
<div class="dockerfile-build-form-element">
|
||||
<div class="cor-loader" ng-show="building"></div>
|
||||
<div 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 ng-show="state == 'starting-build'" class="starting-build">
|
||||
<div class="cor-loader-inline"></div>
|
||||
<div>Please wait while <span class="registry-name" short="true"></span> starts the build</div>
|
||||
</div>
|
||||
<div ng-show="state != 'starting-build'">
|
||||
<div class="file-upload-box"
|
||||
select-message="Please select a Dockerfile or an archive (.tar.gz or .zip) containing a Dockerfile at the root directory"
|
||||
files-cleared="handleFilesCleared()"
|
||||
files-selected="handleFilesSelected(files, callback)"
|
||||
files-validated="handleFilesValidated(uploadFiles)"></div>
|
||||
|
||||
<div class="robot-permission" ng-show="privateBaseRepository && state != 'uploading-files'">
|
||||
<div class="help-text">
|
||||
<p>The selected Dockerfile contains a <code>FROM</code> that refers to private repository <strong>{{ privateBaseRepository }}</strong>.</p>
|
||||
<p>
|
||||
A robot account with read access to that repository is required for the build:
|
||||
</p>
|
||||
</div>
|
||||
<div class="entity-search" namespace="repository.namespace"
|
||||
placeholder="'Select robot account for pulling'"
|
||||
current-entity="pullEntity"
|
||||
pull-right="true"
|
||||
allowed-entities="['robot']"></div>
|
||||
<div class="co-alert co-alert-danger"
|
||||
ng-if="currentRobotHasPermission === false">
|
||||
Robot account <strong>{{ pullEntity.name }}</strong> does not have
|
||||
read permission on repository <strong>{{ privateBaseRepository }}</strong>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="!uploading && !building">
|
||||
<dl>
|
||||
<dt>Dockerfile or <code>.tar.gz</code> or <code>.zip</code>:</dt>
|
||||
<dd>
|
||||
<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 ng-show="privateBaseRepository">
|
||||
<dt>Base Image Pull Credentials:</dt>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
42
static/directives/file-upload-box.html
Normal file
42
static/directives/file-upload-box.html
Normal file
|
@ -0,0 +1,42 @@
|
|||
<div class="file-upload-box-element">
|
||||
<div class="file-input-container">
|
||||
<div ng-show="state != 'uploading'">
|
||||
<input id="file-drop-{{ boxId }}" name="file-drop-{{ boxId }}" class="file-drop" type="file" files-changed="handleFilesChanged(files)">
|
||||
<label for="file-drop-{{ boxId }}" ng-class="state">
|
||||
<span class="chosen-file">
|
||||
<span ng-if="selectedFiles.length">
|
||||
{{ selectedFiles[0].name }}
|
||||
<span ng-if="selectedFiles.length > 1">
|
||||
and {{ selectedFiles.length - 1 }} others...
|
||||
</span>
|
||||
</span>
|
||||
</span><span class="choose-button">
|
||||
<span>Select file</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="cor-loader-line" ng-if="state == 'checking'"></div>
|
||||
|
||||
<div class="status-message" ng-if="state == 'uploading'">
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar" role="progressbar"
|
||||
aria-valuenow="{{ uploadProgress }}" aria-valuemin="0" aria-valuemax="100"
|
||||
style="{{ 'width: ' + uploadProgress + '%' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Uploading file {{ currentlyUploadingFile.name }}...
|
||||
</div>
|
||||
|
||||
<div class="select-message" ng-if="state == 'clear'">{{ selectMessage }}</div>
|
||||
<div class="status-message error-message" ng-if="state == 'error'">
|
||||
<i class="fa fa-times-circle"></i>
|
||||
{{ message }}
|
||||
</div>
|
||||
<div class="status-message okay-message" ng-if="state == 'okay'">
|
||||
<i class="fa fa-check-circle"></i>
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -2,7 +2,7 @@
|
|||
<div class="feedback-bar" feedback="feedback"></div>
|
||||
<div class="tab-header-controls">
|
||||
<button class="btn btn-primary" ng-click="showNewBuildDialog()">
|
||||
<i class="fa fa-plus"></i>Start Build
|
||||
<i class="fa fa-play"></i> Start New Build
|
||||
</button>
|
||||
</div>
|
||||
<h3 class="tab-header">Repository Builds</h3>
|
||||
|
|
|
@ -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({});
|
||||
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
154
static/js/directives/ui/file-upload-box.js
Normal file
154
static/js/directives/ui/file-upload-box.js
Normal 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;
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -133,18 +133,14 @@
|
|||
</div>
|
||||
|
||||
<div class="row" ng-show="repo.initialize == 'dockerfile' || repo.initialize == 'zipfile'">
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="section">
|
||||
<div class="section-title">Initialize from Dockerfile</div>
|
||||
<div style="padding-top: 20px;">
|
||||
<div class="initialize-repo">
|
||||
<div class="dockerfile-build-form" repository="createdForBuild || repo"
|
||||
upload-failed="handleBuildFailed(message)"
|
||||
build-started="handleBuildStarted()"
|
||||
build-failed="handleBuildFailed(message)"
|
||||
start-now="createdForBuild"
|
||||
is-ready="hasDockerfile" uploading="uploading" building="building"></div>
|
||||
<div class="dockerfile-build-form" repository="repo || createdForBuild"
|
||||
is-ready="hasDockerfile"
|
||||
ready-for-build="readyForBuild(startBuild)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -165,8 +161,9 @@
|
|||
|
||||
<div class="col-md-12">
|
||||
<button class="btn btn-large btn-primary" type="submit"
|
||||
ng-disabled="uploading || building || newRepoForm.$invalid || (repo.is_public == '0' && (planRequired || checkingPlan)) || ((repo.initialize == 'dockerfile' || repo.initialize == 'zipfile') && !hasDockerfile)">
|
||||
<i class="fa fa-large" ng-class="repo.is_public == '1' ? 'fa-unlock' : 'fa-lock'" style="margin-right: 4px"></i>
|
||||
ng-disabled="buildStarted || newRepoForm.$invalid || (repo.is_public == '0' && (planRequired || checkingPlan)) || ((repo.initialize == 'dockerfile' || repo.initialize == 'zipfile') && !hasDockerfile)">
|
||||
<i class="fa fa-large" ng-class="repo.is_public == '1' ? 'fa-unlock' : 'fa-lock'"
|
||||
style="margin-right: 4px"></i>
|
||||
Create {{ repo.is_public == '1' ? 'Public' : 'Private' }} Repository
|
||||
</button>
|
||||
</div>
|
||||
|
|
Reference in a new issue