From a049fc57c61b1a66b7617c0caaca7ba35ec9e143 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 4 Feb 2014 20:50:13 -0500 Subject: [PATCH] Start on tour infrastructure. Note that this code works but is NOT STYLED and has a FAKE TEMP TOUR in it --- static/css/quay.css | 32 ++++ static/directives/angular-tour-overlay.html | 10 ++ static/js/app.js | 5 +- static/js/controllers.js | 31 +++- static/js/tour.js | 147 ++++++++++++++++ static/lib/jquery.spotlight.js | 179 ++++++++++++++++++++ static/partials/guide.html | 129 +------------- static/partials/repo-list.html | 5 +- templates/base.html | 2 + templates/index.html | 1 + 10 files changed, 411 insertions(+), 130 deletions(-) create mode 100644 static/directives/angular-tour-overlay.html create mode 100644 static/js/tour.js create mode 100644 static/lib/jquery.spotlight.js diff --git a/static/css/quay.css b/static/css/quay.css index 1fe07fddc..f18a5ca6e 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -2701,4 +2701,36 @@ p.editable:hover i { .contact-options { margin-top: 60px; +} + +/*********************************************/ + +.angular-tour-overlay-element { + display: block; + position: fixed; + bottom: 20px; + left: 20px; + right: 20px; + background: rgba(0, 0, 0, 0.6); + color: white; + padding: 20px; + border-radius: 10px; + z-index: 9999999; + + opacity: 0; + + transition: opacity 750ms ease-in-out; + -webkit-transition: opacity 750ms ease-in-out; +} + +.angular-tour-overlay-element.touring { + opacity: 1; +} + +.angular-tour-overlay-element.nottouring { + pointer-events: none; + position: absolute; + left: -10000px; + width: 0px; + height: 0px; } \ No newline at end of file diff --git a/static/directives/angular-tour-overlay.html b/static/directives/angular-tour-overlay.html new file mode 100644 index 000000000..dbc9249a8 --- /dev/null +++ b/static/directives/angular-tour-overlay.html @@ -0,0 +1,10 @@ +
+ {{ tour.title }} + {{ step.title }} + {{ step.content }} + + + + + +
diff --git a/static/js/app.js b/static/js/app.js index 387b2a735..028392432 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -103,8 +103,8 @@ function getMarkedDown(string) { } // Start the application code itself. -quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', '$strap.directives', 'ngCookies', 'ngSanitize', 'angular-md5'], function($provide, cfpLoadingBarProvider) { - cfpLoadingBarProvider.includeSpinner = false; +quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', '$strap.directives', 'ngCookies', 'ngSanitize', 'angular-md5'], function($provide, cfpLoadingBarProvider) { + cfpLoadingBarProvider.includeSpinner = false; $provide.factory('UtilService', ['$sanitize', function($sanitize) { var utilService = {}; @@ -2510,6 +2510,7 @@ quayApp.directive('ngBlur', function() { }; }); + quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanService', '$http', '$timeout', function($location, $rootScope, Restangular, UserService, PlanService, $http, $timeout) { diff --git a/static/js/controllers.js b/static/js/controllers.js index 8f10ca5ef..1ad325609 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -42,7 +42,36 @@ function PlansCtrl($scope, $location, UserService, PlanService) { }; } -function GuideCtrl($scope) { +function GuideCtrl($scope, AngularTour, AngularTourSignals) { + $scope.startTour = function() { + AngularTour.start({ + 'title': 'My Test Tour', + 'steps': [ + { + 'title': 'Welcome to the tour!', + 'content': 'Some cool content' + }, + { + 'title': 'A step tied to a DOM element', + 'content': 'This is the best DOM element!', + 'element': '#test-element' + }, + { + 'content': 'Waiting for the page to change', + 'signal': AngularTourSignals.matchesLocation('/repository/') + }, + { + 'content': 'Waiting for the page to load', + 'signal': AngularTourSignals.elementAvaliable('*[data-repo="public/publicrepo"]') + }, + { + 'content': 'Now click on the public repository', + 'signal': AngularTourSignals.matchesLocation('/repository/public/publicrepo'), + 'element': '*[data-repo="public/publicrepo"]' + } + ] + }); + }; } function SecurityCtrl($scope) { diff --git a/static/js/tour.js b/static/js/tour.js new file mode 100644 index 000000000..4b26c60df --- /dev/null +++ b/static/js/tour.js @@ -0,0 +1,147 @@ +angular.module("angular-tour", []) + .provider('AngularTour', function() { + this.$get = ['$document', '$rootScope', '$compile', function($document, $rootScope, $compile) { + function _start(tour) { + $rootScope.angular_tour_current = tour; + } + + function _stop() { + $rootScope.angular_tour_current = null; + } + + return { + start: _start, + stop: _stop + }; + + }]; + }) + + .directive('angularTourOverlay', function() { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/angular-tour-overlay.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'tour': '=tour' + }, + controller: function($scope, $element, $interval) { + $scope.stepIndex = 0; + $scope.step = null; + $scope.interval = null; + + var checkSignalTimer = function() { + if (!$scope.step) { + stopSignalTimer(); + return; + } + + var signal = $scope.step.signal; + if (signal()) { + $scope.next(); + } + }; + + var stopSignalTimer = function() { + if (!$scope.interval) { return; } + + $interval.cancel($scope.interval); + $scope.interval = null; + }; + + var startSignalTimer = function() { + $scope.interval = $interval(checkSignalTimer, 500); + }; + + var closeDomHighlight = function() { + if (!$scope.step) { return; } + + var element = $($scope.step.element); + element.spotlight('close'); + }; + + var updateDomHighlight = function() { + var element = $($scope.step.element); + if (!element.length) { + return; + } + + element.spotlight({ + opacity: .5, + speed: 400, + color: '#333', + animate: true, + easing: 'linear', + exitEvent: 'mouseenter', + exitEventAppliesToElement: true, + paddingX: 1, + paddingY: 1 + }); + }; + + $scope.setStepIndex = function(stepIndex) { + // Close existing spotlight and signal timer. + closeDomHighlight(); + stopSignalTimer(); + + // Check if there is a next step. + if (!$scope.tour || stepIndex >= $scope.tour.steps.length) { + $scope.step = null; + $scope.hasNextStep = false; + return; + } + + $scope.step = $scope.tour.steps[stepIndex]; + $scope.stepIndex = stepIndex; + $scope.hasNextStep = stepIndex < $scope.tour.steps.length - 1; + + // Need the timeout here to ensure the click event does not + // hide the spotlight. + setTimeout(function() { + updateDomHighlight(); + }, 1); + + // Start listening for signals to move the tour forward. + if ($scope.step.signal) { + startSignalTimer(); + } + }; + + $scope.stop = function() { + $scope.tour = null; + }; + + $scope.next = function() { + $scope.setStepIndex($scope.stepIndex + 1); + }; + + $scope.$watch('tour', function(tour) { + stopSignalTimer(); + $scope.setStepIndex(0); + }); + } + }; + return directiveDefinitionObject; + }) + + .factory('AngularTourSignals', ['$location', function($location) { + var signals = {}; + + // Signal: When the page location matches the given path. + signals.matchesLocation = function(locationPath) { + return function() { + return $location.path() == locationPath; + }; + }; + + // Signal: When an element is found in the page's DOM. + signals.elementAvaliable = function(elementPath) { + return function() { + return $(elementPath).length > 0; + }; + }; + + return signals; + }]); diff --git a/static/lib/jquery.spotlight.js b/static/lib/jquery.spotlight.js new file mode 100644 index 000000000..f8afe2e0b --- /dev/null +++ b/static/lib/jquery.spotlight.js @@ -0,0 +1,179 @@ +/** + * jQuery Spotlight + * + * Project Page: http://github.com/ + * Original Plugin code by Gilbert Pellegrom (2009) + * Licensed under the GPL license (http://www.gnu.org/licenses/gpl-3.0.html) + * Version 1.1 (2011) + * Modified by jschorr (Fix Opacity bug, fix handling of events) + */ +(function ($) { + var currentOverlay; + + $.fn.spotlight = function (options) { + var method = 'create'; + + // Default settings + settings = $.extend({}, { + opacity: .5, + speed: 400, + color: '#333', + animate: true, + easing: '', + exitEvent: 'click', + exitEventAppliesToElement: false, + onShow: function () { + // do nothing + }, + onHide: function () { + // do nothing + }, + spotlightZIndex: 9999, + spotlightElementClass: 'spotlight-background', + parentSelector: 'html', + paddingX: 0, + paddingY: 0 + }, options); + + function closeOverlay () { + if (!currentOverlay) { + return; + } + + if (settings.animate) { + currentOverlay.animate({opacity: 0}, settings.speed, settings.easing, function () { + currentOverlay.remove(); + currentOverlay = null; + + // Trigger the onHide callback + settings.onHide.call(this); + }); + } else { + currentOverlay.remove(); + currentOverlay = null; + + // Trigger the onHide callback + settings.onHide.call(this); + } + } + + if (typeof options === 'string') { + method = options; + options = arguments[1]; + } + + switch (method) { + case 'close': + case 'destroy': + closeOverlay(); + return; + } + + var elements = $(this), + overlay, + parent, + context; + + /** + * Colour in the overlay and clear all element masks + */ + function fillOverlay () { + context.fillStyle = settings.color; + context.fillRect(0, 0, parent.innerWidth(), parent.innerHeight()); + + // loop through elements and clear their position + elements.each(function (i, e) { + var ej = $(e); + + var currentPos = e.getBoundingClientRect(); + context.clearRect( + currentPos.left - settings.paddingX, + currentPos.top - settings.paddingY, + ej.outerWidth() + (settings.paddingX * 2), + ej.outerHeight() + (settings.paddingY * 2) + ); + }); + } + + /** + * Handle resizing the window + * + * @param e + */ + function handleResize (e) { + overlay.attr('width', parent.innerWidth()); + overlay.attr('height', parent.innerHeight()); + + if (typeof context !== 'undefined') { + fillOverlay(); + } + } + + closeOverlay(); + + // Add the overlay element + overlay = $(''); + overlay.addClass(settings.spotlightElementClass); + + currentOverlay = overlay; + + parent = $(settings.parentSelector); + parent.append(overlay); + + // Get our elements + var element = $(this); + + // Set the CSS styles + var cssConfig = { + position: 'absolute', + top: 0, + left: 0, + height: '100%', + width: '100%', + zIndex: settings.spotlightZIndex, + opacity: 0 + }; + + if (settings.parentSelector == 'html') { + parent.css('height', '100%'); + } + + overlay.css(cssConfig); + handleResize(); + $(window).resize(handleResize); + + context = overlay[0].getContext('2d'); + + fillOverlay(); + + // Fade in the spotlight + if (settings.animate && jQuery.support.opacity) { + overlay.animate({opacity: settings.opacity}, settings.speed, settings.easing, function () { + // Trigger the onShow callback + settings.onShow.call(this); + }); + } else { + if (jQuery.support.opacity) { + overlay.css('opacity', settings.opacity); + } else { + overlay.css('filter', 'alpha(opacity=' + settings.opacity * 100 + ')'); + } + // Trigger the onShow callback + settings.onShow.call(this); + } + + // Set up click to close + if (settings.exitEventAppliesToElement) { + overlay.css({ + pointerEvents: 'none' + }); + element.on(settings.exitEvent, overlay, closeOverlay); + } else { + $(document).on(settings.exitEvent, overlay, closeOverlay); + } + + // Returns the jQuery object to allow for chainability. + return this; + }; + +})(jQuery); diff --git a/static/partials/guide.html b/static/partials/guide.html index b5a018a75..63218a363 100644 --- a/static/partials/guide.html +++ b/static/partials/guide.html @@ -1,127 +1,4 @@ -
-
Warning: Quay requires docker version 0.6.2 or higher to work
- -

User Guide

-
-

Signing into Quay Setup

-
- To setup your Docker client for pushing to Quay, login with your credentials: -

-
$ sudo docker login quay.io
-
-Login against server at https://quay.io/v1/
-Username: myusername
-Password: mypassword
-Email: my@email.com
-
-
- -

Pushing a repository to Quay Requires Write Access

-
- In order to push a repository to Quay, it must be tagged with the quay.io domain and the namespace under which it will live: -

-
sudo docker tag 0u123imageid quay.io/username/repo_name
-
- Once tagged, the repository can be pushed to Quay:

-
sudo docker push quay.io/username/repo_name
-
-
- -

Pulling a repository from Quay

-
-
Note: Private repositories require you to be logged in or the pull will fail. See above for how to sign into Quay if you have never done so before.
- To pull a repository from Quay, run the following command: -

-
sudo docker pull quay.io/username/repo_name
-
-
- -

Granting and managing permissions to users Requires Admin Access

-
-
Quay allows a repository to be shared any number of users and to grant those users any level of permissions for a repository
- -
    -
  • Permissions for a repository can be granted and managed in the repository's admin interface -
  • Adding a user: Type that user's username in the "Add New User..." field, and select the user -
  • Changing permissions: A user's permissions (read, read/write or admin) can be changed by clicking the field to the right of the user -
  • Removing a user: A user can be removed from the list by clicking the X and then clicking Delete -
- -
-
- -

Using robot accounts Requires Admin Access

-
-
- There are many circumstances where permissions for repositories need to be shared across those repositories (continuous integration, etc). - To support this case, Quay allows the use of robot accounts which can be created in the user/organization's admin view and can be - shared by multiple repositories that are owned by that user or organization. -
- -
    -
  • Robot accounts can be managed in the user or organization admin's interface -
  • Adding a robot account: Click "Create Robot Account" and enter a name for the account. The username will become namespace+accountname where "namespace" is the name of the user or organiaztion. -
  • Setting permissions: Permissions can be granted to a robot account in a repository by adding that account like any other user or team. -
  • Deleting a robot account: A robot account can be deleted by clicking the X and then clicking Delete -
  • Using a robot account: To use the robot account, the following credentials can be used: -
    -
    Username
    namespace+accountname (Example: mycompany+deploy)
    -
    Password
    (token value can be found by clicking on the robot account in the admin panel)
    -
    Email
    This value is ignored, any value may be used.
    -
    -
-
- -

Using access tokens in place of users Requires Admin Access

-
-
- For per-repository token authentication, Quay allows the use of access tokens which can be created on a repository and have read and/or write - permissions, without any passwords. -
- -
    -
  • Tokens can be managed in the repository's admin interface -
  • Adding a token: Enter a user-readable description in the "New token description" field -
  • Changing permissions: A token's permissions (read or read/write) can be changed by clicking the field to the right of the token -
  • Deleting a token: A token can be deleted by clicking the X and then clicking Delete -
  • Using a token: To use the token, the following credentials can be used: -
    -
    Username
    $token
    -
    Password
    (token value can be found by clicking on the token)
    -
    Email
    This value is ignored, any value may be used.
    -
    -
-
- - -

Deleting a tag Requires Admin Access

-
-
- A specific tag and all its images can be deleted by right clicking on the tag in the repository history tree and choosing "Delete Tag". This will delete the tag and any images unique to it. Images will not be deleted until all tags sharing them are deleted. -
-
- - - -

Using push webhooks Requires Admin Access

-
- A repository can have one or more push webhooks setup, which will be invoked whenever a successful push occurs. Webhooks can be managed from the repository's admin interface. -

A webhook will be invoked - as an HTTP POST to the specified URL, with a JSON body describing the push:

-
-{
-  "pushed_image_count": 2,
-  "name": "ubuntu",
-  "repository":"devtable/ubuntu",
-  "docker_url": "quay.io/devtable/ubuntu",
-  "updated_tags": {
-    "latest": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc"
-  },
-  "namespace": "devtable",
-  "visibility": "private",
-  "homepage": "https://quay.io/repository/devtable/ubuntu"
-}
-
-
-
+ +
+ This is a test element
diff --git a/static/partials/repo-list.html b/static/partials/repo-list.html index 9b740f768..0b2f70df9 100644 --- a/static/partials/repo-list.html +++ b/static/partials/repo-list.html @@ -50,7 +50,10 @@
diff --git a/templates/base.html b/templates/base.html index 731e19d18..ee09131ec 100644 --- a/templates/base.html +++ b/templates/base.html @@ -72,6 +72,7 @@ window.__token = '{{ csrf_token() }}'; + @@ -170,5 +171,6 @@ var isProd = document.location.hostname === 'quay.io'; {% endif %} +
diff --git a/templates/index.html b/templates/index.html index fc275366d..d3e3de75c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -35,6 +35,7 @@ + {% endblock %} {% block body_content %}