diff --git a/buildstatus/building.svg b/buildstatus/building.svg new file mode 100644 index 000000000..6a2e8fc8c --- /dev/null +++ b/buildstatus/building.svg @@ -0,0 +1 @@ +Docker ImageDocker Imagebuildingbuilding \ No newline at end of file diff --git a/buildstatus/failed.svg b/buildstatus/failed.svg new file mode 100644 index 000000000..1a3d79f75 --- /dev/null +++ b/buildstatus/failed.svg @@ -0,0 +1 @@ +Docker ImageDocker Imagebuild failedbuild failed \ No newline at end of file diff --git a/buildstatus/none.svg b/buildstatus/none.svg new file mode 100644 index 000000000..0f513d9cd --- /dev/null +++ b/buildstatus/none.svg @@ -0,0 +1 @@ +Docker ImageDocker Imagenonenone \ No newline at end of file diff --git a/buildstatus/ready.svg b/buildstatus/ready.svg new file mode 100644 index 000000000..4ea770599 --- /dev/null +++ b/buildstatus/ready.svg @@ -0,0 +1 @@ +Docker ImageDocker Imagereadyready \ No newline at end of file diff --git a/data/database.py b/data/database.py index f245ccb96..413d2261c 100644 --- a/data/database.py +++ b/data/database.py @@ -101,6 +101,7 @@ class Repository(BaseModel): name = CharField() visibility = ForeignKeyField(Visibility) description = TextField(null=True) + badge_token = CharField(default=uuid_generator) class Meta: database = db diff --git a/data/model.py b/data/model.py index e9e695e9f..cd447fe0f 100644 --- a/data/model.py +++ b/data/model.py @@ -1421,6 +1421,21 @@ def list_repository_builds(namespace_name, repository_name, return query +def get_recent_repository_build(namespace_name, repository_name): + query = (RepositoryBuild.select(RepositoryBuild) + .join(Repository) + .where(Repository.name == repository_name, + Repository.namespace == namespace_name) + .order_by(RepositoryBuild.started.desc()) + .limit(1)) + + results = list(query) + if results: + return results[0] + + return None + + def create_repository_build(repo, access_token, job_config_obj, dockerfile_id, display_name, trigger=None): return RepositoryBuild.create(repository=repo, access_token=access_token, diff --git a/endpoints/api.py b/endpoints/api.py index a20d9cc91..443febbee 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -1117,7 +1117,8 @@ def get_repo(namespace, repository): 'can_admin': can_admin, 'is_public': is_public, 'is_building': len(list(active_builds)) > 0, - 'is_organization': bool(organization) + 'is_organization': bool(organization), + 'status_token': repo.badge_token if not is_public else '' }) abort(404) # Not found diff --git a/endpoints/web.py b/endpoints/web.py index 9d898fbf7..0e0692480 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -1,5 +1,6 @@ import logging import stripe +import os from flask import (abort, redirect, request, url_for, make_response, Response, Blueprint) @@ -13,7 +14,7 @@ from util.invoice import renderInvoiceToPdf from util.seo import render_snapshot from util.cache import no_cache from endpoints.common import common_login, render_page_template - +from util.names import parse_repository_name logger = logging.getLogger(__name__) @@ -196,3 +197,39 @@ def confirm_recovery(): return redirect(url_for('web.user')) else: abort(403) + + +@web.route('/repository//status', methods=['GET']) +@parse_repository_name +@no_cache +def build_status_badge(namespace, repository): + token = request.args.get('token', None) + is_public = model.repository_is_public(namespace, repository) + if not is_public: + repo = model.get_repository(namespace, repository) + if not repo or token != repo.badge_token: + abort(404) + + # Lookup the tags for the repository. + tags = model.list_repository_tags(namespace, repository) + + # Lookup the current/recent build. + status = 'none' + build = model.get_recent_repository_build(namespace, repository) + if build: + if build.phase == 'error': + status = 'failed' + elif build.phase == 'complete': + pass + else: + status = 'building' + else: + if list(tags): + status = 'ready' + + with open(os.path.join(app.root_path, 'buildstatus', status + '.svg')) as f: + svg = f.read() + + response = make_response(svg) + response.content_type = 'image/svg+xml' + return response diff --git a/static/css/quay.css b/static/css/quay.css index 43c2e1480..3ddf44b50 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -282,21 +282,6 @@ i.toggle-icon:hover { vertical-align: middle; } -#copyClipboard { - cursor: pointer; -} - -#copyClipboard.zeroclipboard-is-hover { - background: #428bca; - color: white; -} - -#clipboardCopied.hovering { - position: absolute; - right: 0px; - top: 40px; -} - .content-container { padding-bottom: 70px; } @@ -1651,7 +1636,38 @@ p.editable:hover i { margin-top: 28px; } -#clipboardCopied { +.copy-box-element { + position: relative; +} + +.global-zeroclipboard-container embed { + cursor: pointer; +} + +#copyClipboard.zeroclipboard-is-hover, .copy-box-element .zeroclipboard-is-hover { + background: #428bca; + color: white; + cursor: pointer !important; +} + +#clipboardCopied.hovering, .copy-box-element .hovering { + position: absolute; + right: 0px; + top: 40px; + pointer-events: none; + z-index: 100; +} + +.copy-box-element .id-container { + display: inline-block; + vertical-align: middle; +} + +.copy-box-element input { + background-color: white !important; +} + +#clipboardCopied, .clipboard-copied-message { font-size: 0.8em; display: inline-block; margin-right: 10px; @@ -1662,7 +1678,7 @@ p.editable:hover i { border-radius: 4px; } -#clipboardCopied.animated { +#clipboardCopied.animated, .clipboard-copied-message { -webkit-animation: fadeOut 4s ease-in-out 0s 1 forwards; -moz-animation: fadeOut 4s ease-in-out 0s 1 forwards; -ms-animation: fadeOut 4s ease-in-out 0s 1 forwards; diff --git a/static/directives/copy-box.html b/static/directives/copy-box.html new file mode 100644 index 000000000..204bd3a57 --- /dev/null +++ b/static/directives/copy-box.html @@ -0,0 +1,14 @@ +
+
+
+ + + + +
+
+ + +
diff --git a/static/js/app.js b/static/js/app.js index 6b484c41a..d82f9f185 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -963,6 +963,55 @@ quayApp.directive('repoCircle', function () { }); +quayApp.directive('copyBox', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/copy-box.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'value': '=value', + 'hoveringMessage': '=hoveringMessage' + }, + controller: function($scope, $element, $rootScope) { + var number = $rootScope.__copyBoxIdCounter || 0; + $rootScope.__copyBoxIdCounter = number + 1; + $scope.inputId = "copy-box-input-" + number; + + var button = $($element).find('.input-group-addon'); + var input = $($element).find('input'); + + input.attr('id', $scope.inputId); + button.attr('data-clipboard-target', $scope.inputId); + + var clip = new ZeroClipboard($(button), { 'moviePath': 'static/lib/ZeroClipboard.swf' }); + clip.on('complete', function(e) { + var message = $(this.parentNode.parentNode.parentNode).find('.clipboard-copied-message')[0]; + + // Resets the animation. + var elem = message; + elem.style.display = 'none'; + elem.classList.remove('animated'); + + // Show the notification. + setTimeout(function() { + elem.style.display = 'inline-block'; + elem.classList.add('animated'); + }, 10); + + // Reset the notification. + setTimeout(function() { + elem.style.display = 'none'; + }, 5000); + }); + } + }; + return directiveDefinitionObject; +}); + + + quayApp.directive('userSetup', function () { var directiveDefinitionObject = { priority: 0, diff --git a/static/js/controllers.js b/static/js/controllers.js index 4ceb17ab5..fec13dac7 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -1162,6 +1162,30 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams $scope.githubRedirectUri = KeyService.githubRedirectUri; $scope.githubClientId = KeyService.githubClientId; + $scope.getBadgeFormat = function(format, repo) { + if (!repo) { return; } + + var imageUrl = 'https://quay.io/repository/' + namespace + '/' + name + '/status'; + if (!$scope.repo.is_public) { + imageUrl += '?token=' + $scope.repo.status_token; + } + + var linkUrl = 'https://quay.io/repository/' + namespace + '/' + name; + + switch (format) { + case 'svg': + return imageUrl; + + case 'md': + return '[![Docker Repository on Quay.io](' + imageUrl + ')](' + linkUrl + ')'; + + case 'asciidoc': + return 'image:' + imageUrl + '["Docker Repository on Quay.io", link="' + linkUrl + '"]'; + } + + return ''; + }; + $scope.buildEntityForPermission = function(name, permission, kind) { var key = name + ':' + kind; if ($scope.permissionCache[key]) { diff --git a/static/partials/image-view.html b/static/partials/image-view.html index a7b305db2..6eb8383cd 100644 --- a/static/partials/image-view.html +++ b/static/partials/image-view.html @@ -17,20 +17,7 @@
Full Image ID
-
-
-
- - - - -
-
- - -
+
Created
diff --git a/static/partials/repo-admin.html b/static/partials/repo-admin.html index e1cbed845..e800e7d58 100644 --- a/static/partials/repo-admin.html +++ b/static/partials/repo-admin.html @@ -19,6 +19,7 @@