diff --git a/application.py b/application.py index 2244902c1..d3feefcb0 100644 --- a/application.py +++ b/application.py @@ -23,4 +23,4 @@ if application.config.get('INCLUDE_TEST_ENDPOINTS', False): application.debug = True if __name__ == '__main__': - application.run(port=5000, debug=True, host='0.0.0.0') + application.run(port=5000, debug=True, threaded=True, host='0.0.0.0') diff --git a/data/userfiles.py b/data/userfiles.py index c63d8fe53..996e31cf0 100644 --- a/data/userfiles.py +++ b/data/userfiles.py @@ -4,6 +4,11 @@ import logging from boto.s3.key import Key from uuid import uuid4 +import hmac +import time +import urllib +import base64 +import sha logger = logging.getLogger(__name__) @@ -18,9 +23,25 @@ class UserRequestFiles(object): self._s3_conn = boto.s3.connection.S3Connection(s3_access_key, s3_secret_key, is_secure=False) + self._bucket_name = bucket_name self._bucket = self._s3_conn.get_bucket(bucket_name) + self._access_key = s3_access_key + self._secret_key = s3_secret_key self._prefix = 'userfiles' + def prepare_for_drop(self, mimeType): + """ Returns a signed URL to upload a file to our bucket. """ + file_id = str(self._prefix + '/' + str(uuid4())) + + expires = str(int(time.time() + 300)) + signingString = "PUT\n\n" + mimeType + "\n" + expires + "\n/" + self._bucket_name + "/" + file_id; + + hmac_signer = hmac.new(self._secret_key, signingString, sha) + signature = base64.b64encode(hmac_signer.digest()) + + url = "http://s3.amazonaws.com/" + self._bucket_name + "/" + file_id + "?AWSAccessKeyId=" + self._access_key + "&Expires=" + expires + "&Signature=" + urllib.quote(signature); + return (url, file_id) + def store_file(self, flask_file): file_id = str(uuid4()) full_key = os.path.join(self._prefix, file_id) diff --git a/endpoints/api.py b/endpoints/api.py index 82c2bd807..40f3a584e 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -180,22 +180,22 @@ user_files = UserRequestFiles(app.config['AWS_ACCESS_KEY'], app.config['REGISTRY_S3_BUCKET']) -@app.route('/api/repository/', methods=['POST']) +@app.route('/api/repository', methods=['POST']) @api_login_required def create_repo_api(): owner = current_user.db_user() namespace_name = owner.username - repository_name = request.values['repository'] - visibility = request.values['visibility'] + repository_name = request.get_json()['repository'] + visibility = request.get_json()['visibility'] repo = model.create_repository(namespace_name, repository_name, owner, visibility) - resp = make_response('Created', 201) - resp.headers['Location'] = url_for('get_repo_api', namespace=namespace_name, - repository=repository_name) - return resp + return jsonify({ + 'namespace': namespace_name, + 'name': repository_name + }) @app.route('/api/find/repository', methods=['GET']) @@ -395,6 +395,17 @@ def get_repo_builds(namespace, repository): abort(403) # Permissions denied + +@app.route('/api/filedrop/', methods=['POST']) +def get_filedrop_url(): + mimeType = request.get_json()['mimeType'] + (url, file_id) = user_files.prepare_for_drop(mimeType) + return jsonify({ + 'url': url, + 'file_id': file_id + }) + + @app.route('/api/repository//build/', methods=['POST']) @api_login_required @parse_repository_name @@ -402,8 +413,7 @@ def request_repo_build(namespace, repository): permission = ModifyRepositoryPermission(namespace, repository) if permission.can(): logger.debug('User requested repository initialization.') - dockerfile_source = request.files['initializedata'] - dockerfile_id = user_files.store_file(dockerfile_source) + dockerfile_id = request.get_json()['file_id'] repo = model.get_repository(namespace, repository) token = model.create_access_token(repo, 'write') @@ -414,7 +424,9 @@ def request_repo_build(namespace, repository): tag) dockerfile_build_queue.put(json.dumps({'request_id': build_request.id})) - return make_response('Created', 201) + return jsonify({ + 'started': True + }) abort(403) # Permissions denied diff --git a/static/css/quay.css b/static/css/quay.css index f1082c8dc..15fd496c3 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -13,6 +13,30 @@ color: #428bca; } +.build-statuses { +} + +.build-status-container { + padding: 4px; + margin-bottom: 10px; + border-bottom: 1px solid #eee; +} + +.build-status-container .build-message { + display: block; + white-space: nowrap; +} + +.build-status-container .progress { + height: 12px; + margin: 0px; + margin-top: 10px; +} + +.build-status-container:last-child { + margin-bottom: 0px; + border-bottom: 0px solid white; +} .repo-circle { position: relative; @@ -579,6 +603,7 @@ p.editable:hover i { } .repo .description { + margin-top: 10px; margin-bottom: 40px; } @@ -608,22 +633,70 @@ p.editable:hover i { display: inline-block; } +.repo .status-boxes { + float: right; + margin-bottom: 20px; +} + +.repo .status-boxes .status-box { + cursor: pointer; + display: inline-block; + border: 1px solid #eee; + border-radius: 4px; +} + +.repo .status-boxes .status-box .title { + padding: 4px; + display: inline-block; + padding-left: 10px; + padding-right: 10px; +} + +.repo .status-boxes .status-box .title i { + margin-right: 6px; +} + +.repo .status-boxes .status-box .count { + display: inline-block; + background-image: linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%); + padding: 4px; + padding-left: 10px; + padding-right: 10px; + font-weight: bold; + + transform: scaleX(0); + -webkit-transform: scaleX(0); + -moz-transform: scaleX(0); + + transition: transform 500ms ease-in-out; + -webkit-transition: -webkit-transform 500ms ease-in-out; + -moz-transition: -moz-transform 500ms ease-in-out; +} + +.repo .status-boxes .status-box .count.visible { + transform: scaleX(1); + -webkit-transform: scaleX(1); + -moz-transform: scaleX(1); +} + .repo .pull-command { float: right; display: inline-block; - font-size: 1.2em; + font-size: 0.8em; position: relative; - margin-right: 10px; + margin-top: 30px; + margin-right: 26px; } -.repo .pull-command .pull-container { +.repo .pull-container { display: inline-block; width: 300px; + margin-left: 10px; margin-right: 10px; vertical-align: middle; } -.repo .pull-command input { +.repo .pull-container input { cursor: default; background: white; color: #666; @@ -788,8 +861,22 @@ p.editable:hover i { cursor: pointer; } -.repo .description p { - margin-bottom: 6px; + +.repo .build-info { + padding: 10px; + margin: 0px; +} + +.repo .build-info .progress { + margin: 0px; + margin-top: 10px; +} + +.repo .section { + display: block; + margin-bottom: 20px; + padding-bottom: 20px; + border-bottom: 1px solid #eee; } .repo .description p:last-child { @@ -967,7 +1054,7 @@ p.editable:hover i { background: rgb(253, 191, 191); } -.repo-admin .repo-access-state .state-icon i.fa-unlock-alt { +.repo-admin .repo-access-state .state-icon i.fa-unlock { background: rgb(170, 236, 170); } diff --git a/static/directives/build-status.html b/static/directives/build-status.html new file mode 100644 index 000000000..e1c1e2ce6 --- /dev/null +++ b/static/directives/build-status.html @@ -0,0 +1,8 @@ +
+ {{ getBuildMessage(build) }} +
+
+
+
+ +
diff --git a/static/js/app.js b/static/js/app.js index 568c19637..ae555c2d4 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1,5 +1,5 @@ // Start the application code itself. -quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics', 'angulartics.mixpanel'], function($provide) { +quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics', 'angulartics.mixpanel', '$strap.directives'], function($provide) { $provide.factory('UserService', ['Restangular', function(Restangular) { var userResponse = { verified: false, @@ -177,7 +177,72 @@ quayApp.directive('repoCircle', function () { 'repo': '=repo' }, controller: function($scope, $element) { - window.console.log($scope); + } + }; + return directiveDefinitionObject; +}); + + +quayApp.directive('buildStatus', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/build-status.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'build': '=build' + }, + controller: function($scope, $element) { + $scope.getBuildProgress = function(buildInfo) { + switch (buildInfo.status) { + case 'building': + return (buildInfo.current_command / buildInfo.total_commands) * 100; + break; + + case 'pushing': + return (buildInfo.current_image / buildInfo.total_images) * 100; + break; + + case 'complete': + return 100; + break; + + case 'initializing': + case 'starting': + case 'waiting': + return 0; + break; + } + + return -1; + }; + + $scope.getBuildMessage = function(buildInfo) { + switch (buildInfo.status) { + case 'initializing': + return 'Starting Dockerfile build'; + break; + + case 'starting': + case 'waiting': + case 'building': + return 'Building image from Dockerfile'; + break; + + case 'pushing': + return 'Pushing image built from Dockerfile'; + break; + + case 'complete': + return 'Dockerfile build completed and pushed'; + break; + + case 'error': + return 'Dockerfile build failed: ' + buildInfo.message; + break; + } + }; } }; return directiveDefinitionObject; diff --git a/static/js/controllers.js b/static/js/controllers.js index e867ac75d..1e5493ec3 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -339,6 +339,19 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location, $tim } }); + var startBuildInfoTimer = function(repo) { + setInterval(function() { + $scope.$apply(function() { getBuildInfo(repo); }); + }, 5000); + }; + + var getBuildInfo = function(repo) { + var buildInfo = Restangular.one('repository/' + repo.namespace + '/' + repo.name + '/build/'); + buildInfo.get().then(function(resp) { + $scope.buildsInfo = resp.builds; + }); + }; + var listImages = function() { if ($scope.imageHistory) { return; } @@ -443,6 +456,10 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location, $tim $('#copyClipboard').clipboardCopy(); $scope.loading = false; + + if (repo.is_building) { + getBuildInfo(repo); + } }, function() { $scope.repo = null; $scope.loading = false; @@ -895,18 +912,83 @@ function V1Ctrl($scope, UserService) { }; } -function NewRepoCtrl($scope, UserService) { +function NewRepoCtrl($scope, $location, UserService, Restangular) { $scope.repo = { 'is_public': 1, 'description': '', 'initialize': false }; + var startBuild = function(repo, fileId) { + $scope.building = true; + + var data = { + 'file_id': fileId + }; + + var startBuildCall = Restangular.one('repository/' + repo.namespace + '/' + repo.name + '/build/'); + startBuildCall.customPOST(data).then(function(resp) { + $location.path('/repository/' + repo.namespace + '/' + repo.name); + }, function() { + $('#couldnotbuildModal').modal(); + $location.path('/repository/' + repo.namespace + '/' + repo.name); + }); + }; + + var conductUpload = function(repo, file, url, fileId, mimeType) { + var request = new XMLHttpRequest(); + request.open('PUT', url, true); + request.overrideMimeType(mimeType); + request.onprogress = function(e) { + var percentLoaded; + if (e.lengthComputable) { + $scope.upload_progress = (e.loaded / e.total) * 100; + } + }; + request.onerror = function() { + $('#couldnotbuildModal').modal(); + $location.path('/repository/' + repo.namespace + '/' + repo.name); + }; + request.onreadystatechange = function() { + var state = request.readyState; + if (state == 4) { + $scope.$apply(function() { + $scope.uploading = false; + startBuild(repo, fileId); + }); + return; + } + }; + request.send(file); + }; + + var startFileUpload = function(repo) { + $scope.uploading = true; + $scope.uploading_progress = 0; + + var uploader = $('#file-drop')[0]; + var file = uploader.files[0]; + $scope.upload_file = file.name; + + var mimeType = file.type || 'application/octet-stream'; + var data = { + 'mimeType': mimeType + }; + + var getUploadUrl = Restangular.one('filedrop/'); + getUploadUrl.customPOST(data).then(function(resp) { + conductUpload(repo, file, resp.url, resp.file_id, mimeType); + }, function() { + $('#couldnotbuildModal').modal(); + $location.path('/repository/' + repo.namespace + '/' + repo.name); + }); + }; + $scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) { $scope.user = currentUser; if ($scope.user.anonymous) { - document.location = '/signin/'; + $location.path('/signin/'); } }, true); @@ -931,4 +1013,36 @@ function NewRepoCtrl($scope, UserService) { $('#editModal').modal('hide'); $scope.repo.description = $('#wmd-input-description')[0].value; }; + + $scope.createNewRepo = function() { + var uploader = $('#file-drop')[0]; + if ($scope.repo.initialize && uploader.files.length < 1) { + $('#missingfileModal').modal(); + return; + } + + $scope.creating = true; + var repo = $scope.repo; + var data = { + 'repository': repo.name, + 'visibility': repo.is_public == '1' ? 'public' : 'private' + }; + + var createPost = Restangular.one('repository'); + createPost.customPOST(data).then(function(created) { + $scope.creating = false; + + // Repository created. Start the upload process if applicable. + if ($scope.repo.initialize) { + startFileUpload(created); + return; + } + + // Otherwise, redirect to the repo page. + $location.path('/repository/' + created.namespace + '/' + created.name); + }, function() { + $('#cannotcreateModal').modal(); + $scope.creating = false; + }); + }; } \ No newline at end of file diff --git a/static/lib/angular-strap.min.js b/static/lib/angular-strap.min.js new file mode 100644 index 000000000..7bf8788fb --- /dev/null +++ b/static/lib/angular-strap.min.js @@ -0,0 +1,8 @@ +/** + * AngularStrap - Twitter Bootstrap directives for AngularJS + * @version v0.7.5 - 2013-07-21 + * @link http://mgcrea.github.com/angular-strap + * @author Olivier Louvignes + * @license MIT License, http://www.opensource.org/licenses/MIT + */ +angular.module("$strap.config",[]).value("$strapConfig",{}),angular.module("$strap.filters",["$strap.config"]),angular.module("$strap.directives",["$strap.config"]),angular.module("$strap",["$strap.filters","$strap.directives","$strap.config"]),angular.module("$strap.directives").directive("bsAlert",["$parse","$timeout","$compile",function(t,e,n){return{restrict:"A",link:function(a,i,o){var r=t(o.bsAlert),s=(r.assign,r(a)),l=function(t){e(function(){i.alert("close")},1*t)};o.bsAlert?a.$watch(o.bsAlert,function(t,e){s=t,i.html((t.title?""+t.title+" ":"")+t.content||""),t.closed&&i.hide(),n(i.contents())(a),(t.type||e.type)&&(e.type&&i.removeClass("alert-"+e.type),t.type&&i.addClass("alert-"+t.type)),angular.isDefined(t.closeAfter)?l(t.closeAfter):o.closeAfter&&l(o.closeAfter),(angular.isUndefined(o.closeButton)||"0"!==o.closeButton&&"false"!==o.closeButton)&&i.prepend('')},!0):((angular.isUndefined(o.closeButton)||"0"!==o.closeButton&&"false"!==o.closeButton)&&i.prepend(''),o.closeAfter&&l(o.closeAfter)),i.addClass("alert").alert(),i.hasClass("fade")&&(i.removeClass("in"),setTimeout(function(){i.addClass("in")}));var u=o.ngRepeat&&o.ngRepeat.split(" in ").pop();i.on("close",function(t){var e;u?(t.preventDefault(),i.removeClass("in"),e=function(){i.trigger("closed"),a.$parent&&a.$parent.$apply(function(){for(var t=u.split("."),e=a.$parent,n=0;t.length>n;++n)e&&(e=e[t[n]]);e&&e.splice(a.$index,1)})},$.support.transition&&i.hasClass("fade")?i.on($.support.transition.end,e):e()):s&&(t.preventDefault(),i.removeClass("in"),e=function(){i.trigger("closed"),a.$apply(function(){s.closed=!0})},$.support.transition&&i.hasClass("fade")?i.on($.support.transition.end,e):e())})}}}]),angular.module("$strap.directives").directive("bsButton",["$parse","$timeout",function(t){return{restrict:"A",require:"?ngModel",link:function(e,n,a,i){if(i){n.parent('[data-toggle="buttons-checkbox"], [data-toggle="buttons-radio"]').length||n.attr("data-toggle","button");var o=!!e.$eval(a.ngModel);o&&n.addClass("active"),e.$watch(a.ngModel,function(t,e){var a=!!t,i=!!e;a!==i?$.fn.button.Constructor.prototype.toggle.call(r):a&&!o&&n.addClass("active")})}n.hasClass("btn")||n.on("click.button.data-api",function(){n.button("toggle")}),n.button();var r=n.data("button");r.toggle=function(){if(!i)return $.fn.button.Constructor.prototype.toggle.call(this);var a=n.parent('[data-toggle="buttons-radio"]');a.length?(n.siblings("[ng-model]").each(function(n,a){t($(a).attr("ng-model")).assign(e,!1)}),e.$digest(),i.$modelValue||(i.$setViewValue(!i.$modelValue),e.$digest())):e.$apply(function(){i.$setViewValue(!i.$modelValue)})}}}}]).directive("bsButtonsCheckbox",["$parse",function(){return{restrict:"A",require:"?ngModel",compile:function(t){t.attr("data-toggle","buttons-checkbox").find("a, button").each(function(t,e){$(e).attr("bs-button","")})}}}]).directive("bsButtonsRadio",["$timeout",function(t){return{restrict:"A",require:"?ngModel",compile:function(e,n){return e.attr("data-toggle","buttons-radio"),n.ngModel||e.find("a, button").each(function(t,e){$(e).attr("bs-button","")}),function(e,n,a,i){i&&(t(function(){n.find("[value]").button().filter('[value="'+i.$viewValue+'"]').addClass("active")}),n.on("click.button.data-api",function(t){e.$apply(function(){i.$setViewValue($(t.target).closest("button").attr("value"))})}),e.$watch(a.ngModel,function(t,i){if(t!==i){var o=n.find('[value="'+e.$eval(a.ngModel)+'"]');o.length&&o.button("toggle")}}))}}}}]),angular.module("$strap.directives").directive("bsButtonSelect",["$parse","$timeout",function(t){return{restrict:"A",require:"?ngModel",link:function(e,n,a,i){var o=t(a.bsButtonSelect);o.assign,i&&(n.text(e.$eval(a.ngModel)),e.$watch(a.ngModel,function(t){n.text(t)}));var r,s,l,u;n.bind("click",function(){r=o(e),s=i?e.$eval(a.ngModel):n.text(),l=r.indexOf(s),u=l>r.length-2?r[0]:r[l+1],e.$apply(function(){n.text(u),i&&i.$setViewValue(u)})})}}}]),angular.module("$strap.directives").directive("bsDatepicker",["$timeout","$strapConfig",function(t,e){var n=/(iP(a|o)d|iPhone)/g.test(navigator.userAgent),a=function a(t){return t=t||"en",{"/":"[\\/]","-":"[-]",".":"[.]"," ":"[\\s]",dd:"(?:(?:[0-2]?[0-9]{1})|(?:[3][01]{1}))",d:"(?:(?:[0-2]?[0-9]{1})|(?:[3][01]{1}))",mm:"(?:[0]?[1-9]|[1][012])",m:"(?:[0]?[1-9]|[1][012])",DD:"(?:"+$.fn.datepicker.dates[t].days.join("|")+")",D:"(?:"+$.fn.datepicker.dates[t].daysShort.join("|")+")",MM:"(?:"+$.fn.datepicker.dates[t].months.join("|")+")",M:"(?:"+$.fn.datepicker.dates[t].monthsShort.join("|")+")",yyyy:"(?:(?:[1]{1}[0-9]{1}[0-9]{1}[0-9]{1})|(?:[2]{1}[0-9]{3}))(?![[0-9]])",yy:"(?:(?:[0-9]{1}[0-9]{1}))(?![[0-9]])"}},i=function i(t,e){var n,i=t,o=a(e);return n=0,angular.forEach(o,function(t,e){i=i.split(e).join("${"+n+"}"),n++}),n=0,angular.forEach(o,function(t){i=i.split("${"+n+"}").join(t),n++}),RegExp("^"+i+"$",["i"])};return{restrict:"A",require:"?ngModel",link:function(t,a,o,r){var s=angular.extend({autoclose:!0},e.datepicker||{}),l=o.dateType||s.type||"date";angular.forEach(["format","weekStart","calendarWeeks","startDate","endDate","daysOfWeekDisabled","autoclose","startView","minViewMode","todayBtn","todayHighlight","keyboardNavigation","language","forceParse"],function(t){angular.isDefined(o[t])&&(s[t]=o[t])});var u=s.language||"en",c=o.dateFormat||s.format||$.fn.datepicker.dates[u]&&$.fn.datepicker.dates[u].format||"mm/dd/yyyy",d=n?"yyyy-mm-dd":c,p=i(d,u);r&&(r.$formatters.unshift(function(t){return"date"===l&&angular.isString(t)&&t?$.fn.datepicker.DPGlobal.parseDate(t,$.fn.datepicker.DPGlobal.parseFormat(c),u):t}),r.$parsers.unshift(function(t){return t?"date"===l&&angular.isDate(t)?(r.$setValidity("date",!0),t):angular.isString(t)&&p.test(t)?(r.$setValidity("date",!0),n?new Date(t):"string"===l?t:$.fn.datepicker.DPGlobal.parseDate(t,$.fn.datepicker.DPGlobal.parseFormat(d),u)):(r.$setValidity("date",!1),void 0):(r.$setValidity("date",!0),null)}),r.$render=function(){if(n){var t=r.$viewValue?$.fn.datepicker.DPGlobal.formatDate(r.$viewValue,$.fn.datepicker.DPGlobal.parseFormat(d),u):"";return a.val(t),t}return r.$viewValue||a.val(""),a.datepicker("update",r.$viewValue)}),n?a.prop("type","date").css("-webkit-appearance","textfield"):(r&&a.on("changeDate",function(e){t.$apply(function(){r.$setViewValue("string"===l?a.val():e.date)})}),a.datepicker(angular.extend(s,{format:d,language:u})),t.$on("$destroy",function(){var t=a.data("datepicker");t&&(t.picker.remove(),a.data("datepicker",null))}),o.$observe("startDate",function(t){a.datepicker("setStartDate",t)}),o.$observe("endDate",function(t){a.datepicker("setEndDate",t)}));var f=a.siblings('[data-toggle="datepicker"]');f.length&&f.on("click",function(){a.prop("disabled")||a.trigger("focus")})}}}]),angular.module("$strap.directives").directive("bsDropdown",["$parse","$compile","$timeout",function(t,e,n){var a=function(t,e){return e||(e=['"]),angular.forEach(t,function(t,n){if(t.divider)return e.splice(n+1,0,'
  • ');var i=""+'"+(t.text||"")+"";t.submenu&&t.submenu.length&&(i+=a(t.submenu).join("\n")),i+="",e.splice(n+1,0,i)}),e};return{restrict:"EA",scope:!0,link:function(i,o,r){var s=t(r.bsDropdown),l=s(i);n(function(){!angular.isArray(l);var t=angular.element(a(l).join(""));t.insertAfter(o),e(o.next("ul.dropdown-menu"))(i)}),o.addClass("dropdown-toggle").attr("data-toggle","dropdown")}}}]),angular.module("$strap.directives").factory("$modal",["$rootScope","$compile","$http","$timeout","$q","$templateCache","$strapConfig",function(t,e,n,a,i,o,r){var s=function s(s){function l(s){var l=angular.extend({show:!0},r.modal,s),u=l.scope?l.scope:t.$new(),c=l.template;return i.when(o.get(c)||n.get(c,{cache:!0}).then(function(t){return t.data})).then(function(t){var n=c.replace(".html","").replace(/[\/|\.|:]/g,"-")+"-"+u.$id,i=$('').attr("id",n).addClass("fade").html(t);return l.modalClass&&i.addClass(l.modalClass),$("body").append(i),a(function(){e(i)(u)}),u.$modal=function(t){i.modal(t)},angular.forEach(["show","hide"],function(t){u[t]=function(){i.modal(t)}}),u.dismiss=u.hide,angular.forEach(["show","shown","hide","hidden"],function(t){i.on(t,function(e){u.$emit("modal-"+t,e)})}),i.on("shown",function(){$("input[autofocus], textarea[autofocus]",i).first().trigger("focus")}),i.on("hidden",function(){l.persist||u.$destroy()}),u.$on("$destroy",function(){i.remove()}),i.modal(l),i})}return new l(s)};return s}]).directive("bsModal",["$q","$modal",function(t,e){return{restrict:"A",scope:!0,link:function(n,a,i){var o={template:n.$eval(i.bsModal),persist:!0,show:!1,scope:n};angular.forEach(["modalClass","backdrop","keyboard"],function(t){angular.isDefined(i[t])&&(o[t]=i[t])}),t.when(e(o)).then(function(t){a.attr("data-target","#"+t.attr("id")).attr("data-toggle","modal")})}}}]),angular.module("$strap.directives").directive("bsNavbar",["$location",function(t){return{restrict:"A",link:function(e,n){e.$watch(function(){return t.path()},function(t){$("li[data-match-route]",n).each(function(e,n){var a=angular.element(n),i=a.attr("data-match-route"),o=RegExp("^"+i+"$",["i"]);o.test(t)?a.addClass("active").find(".collapse.in").collapse("hide"):a.removeClass("active")})})}}}]),angular.module("$strap.directives").directive("bsPopover",["$parse","$compile","$http","$timeout","$q","$templateCache",function(t,e,n,a,i,o){return $("body").on("keyup",function(t){27===t.keyCode&&$(".popover.in").each(function(){$(this).popover("hide")})}),{restrict:"A",scope:!0,link:function(r,s,l){var u=t(l.bsPopover),c=(u.assign,u(r)),d={};angular.isObject(c)&&(d=c),i.when(d.content||o.get(c)||n.get(c,{cache:!0})).then(function(t){angular.isObject(t)&&(t=t.data),l.unique&&s.on("show",function(){$(".popover.in").each(function(){var t=$(this),e=t.data("bs.popover");e&&!e.$element.is(s)&&t.popover("hide")})}),l.hide&&r.$watch(l.hide,function(t,e){t?n.hide():t!==e&&n.show()}),l.show&&r.$watch(l.show,function(t,e){t?a(function(){n.show()}):t!==e&&n.hide()}),s.popover(angular.extend({},d,{content:t,html:!0}));var n=s.data("bs.popover");n.hasContent=function(){return this.getTitle()||t},n.getPosition=function(){var t=$.fn.popover.Constructor.prototype.getPosition.apply(this,arguments);return e(this.$tip)(r),r.$digest(),this.$tip.data("bs.popover",this),t},r.$popover=function(t){n(t)},angular.forEach(["show","hide"],function(t){r[t]=function(){n[t]()}}),r.dismiss=r.hide,angular.forEach(["show","shown","hide","hidden"],function(t){s.on(t,function(e){r.$emit("popover-"+t,e)})})})}}}]),angular.module("$strap.directives").directive("bsSelect",["$timeout",function(t){return{restrict:"A",require:"?ngModel",link:function(e,n,a,i){var o=e.$eval(a.bsSelect)||{};t(function(){n.selectpicker(o),n.next().removeClass("ng-scope")}),i&&e.$watch(a.ngModel,function(t,e){angular.equals(t,e)||n.selectpicker("refresh")})}}}]),angular.module("$strap.directives").directive("bsTabs",["$parse","$compile","$timeout",function(t,e,n){var a='
    ';return{restrict:"A",require:"?ngModel",priority:0,scope:!0,template:a,replace:!0,transclude:!0,compile:function(){return function(e,a,i,o){var r=t(i.bsTabs);r.assign,r(e),e.panes=[];var s,l,u,c=a.find("ul.nav-tabs"),d=a.find("div.tab-content"),p=0;n(function(){d.find("[data-title], [data-tab]").each(function(t){var n=angular.element(this);s="tab-"+e.$id+"-"+t,l=n.data("title")||n.data("tab"),u=!u&&n.hasClass("active"),n.attr("id",s).addClass("tab-pane"),i.fade&&n.addClass("fade"),e.panes.push({id:s,title:l,content:this.innerHTML,active:u})}),e.panes.length&&!u&&(d.find(".tab-pane:first-child").addClass("active"+(i.fade?" in":"")),e.panes[0].active=!0)}),o&&(a.on("show",function(t){var n=$(t.target);e.$apply(function(){o.$setViewValue(n.data("index"))})}),e.$watch(i.ngModel,function(t){angular.isUndefined(t)||(p=t,setTimeout(function(){var e=$(c[0].querySelectorAll("li")[1*t]);e.hasClass("active")||e.children("a").tab("show")}))}))}}}}]),angular.module("$strap.directives").directive("bsTimepicker",["$timeout","$strapConfig",function(t,e){var n="((?:(?:[0-1][0-9])|(?:[2][0-3])|(?:[0-9])):(?:[0-5][0-9])(?::[0-5][0-9])?(?:\\s?(?:am|AM|pm|PM))?)";return{restrict:"A",require:"?ngModel",link:function(a,i,o,r){if(r){i.on("changeTime.timepicker",function(){t(function(){r.$setViewValue(i.val())})});var s=RegExp("^"+n+"$",["i"]);r.$parsers.unshift(function(t){return!t||s.test(t)?(r.$setValidity("time",!0),t):(r.$setValidity("time",!1),void 0)})}i.attr("data-toggle","timepicker"),i.parent().addClass("bootstrap-timepicker"),i.timepicker(e.timepicker||{});var l=i.data("timepicker"),u=i.siblings('[data-toggle="timepicker"]');u.length&&u.on("click",$.proxy(l.showWidget,l))}}}]),angular.module("$strap.directives").directive("bsTooltip",["$parse","$compile",function(t){return{restrict:"A",scope:!0,link:function(e,n,a){var i=t(a.bsTooltip),o=(i.assign,i(e));e.$watch(a.bsTooltip,function(t,e){t!==e&&(o=t)}),a.unique&&n.on("show",function(){$(".tooltip.in").each(function(){var t=$(this),e=t.data("tooltip");e&&!e.$element.is(n)&&t.tooltip("hide")})}),n.tooltip({title:function(){return angular.isFunction(o)?o.apply(null,arguments):o},html:!0});var r=n.data("tooltip");r.show=function(){var t=$.fn.tooltip.Constructor.prototype.show.apply(this,arguments);return this.tip().data("tooltip",this),t},e._tooltip=function(t){n.tooltip(t)},e.hide=function(){n.tooltip("hide")},e.show=function(){n.tooltip("show")},e.dismiss=e.hide}}}]),angular.module("$strap.directives").directive("bsTypeahead",["$parse",function(t){return{restrict:"A",require:"?ngModel",link:function(e,n,a,i){var o=t(a.bsTypeahead),r=(o.assign,o(e));e.$watch(a.bsTypeahead,function(t,e){t!==e&&(r=t)}),n.attr("data-provide","typeahead"),n.typeahead({source:function(){return angular.isFunction(r)?r.apply(null,arguments):r},minLength:a.minLength||1,items:a.items,updater:function(t){return i&&e.$apply(function(){i.$setViewValue(t)}),e.$emit("typeahead-updated",t),t}});var s=n.data("typeahead");s.lookup=function(){var t;return this.query=this.$element.val()||"",this.query.length +
    +
    +
    +
    diff --git a/static/partials/new-repo.html b/static/partials/new-repo.html index bc6c8becb..cde232546 100644 --- a/static/partials/new-repo.html +++ b/static/partials/new-repo.html @@ -1,4 +1,21 @@ -
    +
    + +
    + +
    + +
    + +
    + Uploading file {{ upload_file }} +
    +
    +
    +
    + +
    + +
    @@ -65,7 +82,7 @@ Upload a Dockerfile or a zip file containing a Dockerfile in the root directory
    - +
    @@ -74,7 +91,7 @@
    - +
    @@ -105,3 +122,60 @@ + + + + + + + + + + + + diff --git a/static/partials/repo-admin.html b/static/partials/repo-admin.html index f8c2a0428..cda0c8470 100644 --- a/static/partials/repo-admin.html +++ b/static/partials/repo-admin.html @@ -124,7 +124,7 @@
    -
    +
    This repository is currently public and is visible to all users, and may be pulled by all users. diff --git a/static/partials/view-repo.html b/static/partials/view-repo.html index 671b06fba..a10c609a3 100644 --- a/static/partials/view-repo.html +++ b/static/partials/view-repo.html @@ -20,11 +20,10 @@ - + -
    - Get this repository: - +