From dbed1300ad162b71016afd7f21aadc9709b4ff79 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 5 Feb 2014 21:00:04 -0500 Subject: [PATCH] Start on work towards the tutorial. Note that this code is BROKEN --- endpoints/web.py | 6 + static/css/quay.css | 144 +++++++++++++++++++- static/directives/angular-tour-overlay.html | 10 -- static/directives/angular-tour-ui.html | 27 ++++ static/directives/header-bar.html | 1 + static/js/app.js | 2 + static/js/controllers.js | 83 +++++++---- static/js/tour.js | 103 +++++++++++--- static/partials/tutorial.html | 3 + static/tutorial/docker-login.html | 8 ++ static/tutorial/signup.html | 11 ++ static/tutorial/welcome.html | 1 + templates/base.html | 2 +- test/data/test.db | Bin 141312 -> 143360 bytes 14 files changed, 338 insertions(+), 63 deletions(-) delete mode 100644 static/directives/angular-tour-overlay.html create mode 100644 static/directives/angular-tour-ui.html create mode 100644 static/partials/tutorial.html create mode 100644 static/tutorial/docker-login.html create mode 100644 static/tutorial/signup.html create mode 100644 static/tutorial/welcome.html diff --git a/endpoints/web.py b/endpoints/web.py index 57d15164a..6cfd4a438 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -63,6 +63,12 @@ def guide(): return index('') +@web.route('/tutorial/') +@no_cache +def tutorial(): + return index('') + + @web.route('/organizations/') @web.route('/organizations/new/') @no_cache diff --git a/static/css/quay.css b/static/css/quay.css index f18a5ca6e..c4bb6bc6b 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -2705,32 +2705,162 @@ p.editable:hover i { /*********************************************/ -.angular-tour-overlay-element { +.angular-tour-ui-element.overlay { 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; + background: white; + -webkit-box-shadow: 0 5px 15px rgba(0,0,0,0.5); + box-shadow: 0 5px 15px rgba(0,0,0,0.5); + opacity: 0; transition: opacity 750ms ease-in-out; -webkit-transition: opacity 750ms ease-in-out; + + min-width: 400px; } -.angular-tour-overlay-element.touring { +.angular-tour-ui-element.overlay.touring { opacity: 1; } -.angular-tour-overlay-element.nottouring { +.angular-tour-ui-element.overlay.nottouring { pointer-events: none; position: absolute; left: -10000px; width: 0px; height: 0px; +} + +.angular-tour-ui-element.overlay .tour-title { + background: #778EA2; + color: white; + padding: 4px; + padding-left: 6px; + padding-right: 6px; + border-radius: 4px; + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; +} + +.angular-tour-ui-element.overlay .tour-title h4 { + display: inline-block; + font-size: 16px; + margin: 0px; + padding: 2px; +} + +.angular-tour-ui-element.overlay .step-title { + font-size: 20px; +} + +.angular-tour-ui-element.overlay .step-content { + padding: 10px; + padding-left: 0px; + font-size: 16px; +} + +.angular-tour-ui-element.overlay .tour-contents { + padding: 10px; +} + +.angular-tour-ui-element.overlay .controls { + text-align: right; +} + +.angular-tour-ui-element.overlay .controls .btn { + font-size: 16px; +} + + +.angular-tour-ui-element.overlay .fa { + display: none; +} + + +/**************************************************/ + +.angular-tour-ui-element.inline { +} + +.angular-tour-ui-element.inline .fa-dot-circle-o { + font-size: 34px; + background: #ddd; + border-radius: 50%; + width: 40px; + height: 40px; + text-align: center; + padding-top: 4px; + vertical-align: middle; + margin-right: 10px; +} + +.angular-tour-ui-element.inline .tour-title h4 { + font-size: 28px; + padding-bottom: 10px; + margin-bottom: 20px; + border-bottom: 1px solid #eee; +} + +.angular-tour-ui-element.inline .tour-title .close { + display: none; +} + + +.angular-tour-ui-element.inline .step-title { + font-size: 20px; + margin-bottom: 10px; +} + +.angular-tour-ui-element.inline .step-content { + margin-bottom: 10px; +} + +.angular-tour-ui-element.inline .controls { + margin-top: 20px; + border-top: 1px solid #eee; + padding-top: 10px; +} + +.angular-tour-ui-element p { + margin-bottom: 10px; +} + +.angular-tour-ui-element .wait-message { + font-size: 16px; + color: #999; +} + +.angular-tour-ui-element .wait-message .quay-spinner { + display: inline-block; + vertical-align: middle; + margin-right: 6px; +} + +.angular-tour-ui-element .wait-message .quay-spinner .small-spinner { + border-top-color: #999; + border-left-color: #999; +} + +pre.command { + padding: 20px; + background: #fff; + text-shadow: none; + overflow: auto; + border: solid 1px #ccc; + position: relative; + margin-top: 20px; +} + +pre.command:before { +content: "\f120"; +font-family: "FontAwesome"; +font-size: 16px; +margin-right: 6px; +color: #999; } \ No newline at end of file diff --git a/static/directives/angular-tour-overlay.html b/static/directives/angular-tour-overlay.html deleted file mode 100644 index dbc9249a8..000000000 --- a/static/directives/angular-tour-overlay.html +++ /dev/null @@ -1,10 +0,0 @@ -
- {{ tour.title }} - {{ step.title }} - {{ step.content }} - - - - - -
diff --git a/static/directives/angular-tour-ui.html b/static/directives/angular-tour-ui.html new file mode 100644 index 000000000..c357565c1 --- /dev/null +++ b/static/directives/angular-tour-ui.html @@ -0,0 +1,27 @@ +
+ +
+

{{ tour.title }}

+ +
+ +
+
{{ step.title }}
+
+
+
+ +
+
+ +
+ + +
+ +
+
{{ step.waitMessage }} +
+
+
diff --git a/static/directives/header-bar.html b/static/directives/header-bar.html index 32ee88d23..7dcf217f4 100644 --- a/static/directives/header-bar.html +++ b/static/directives/header-bar.html @@ -16,6 +16,7 @@ diff --git a/static/js/app.js b/static/js/app.js index 028392432..5548d9ef1 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -771,6 +771,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu reloadOnSearch: false, controller: UserAdminCtrl}). when('/guide/', {title: 'Guide', description:'Guide to using private docker repositories on Quay.io', templateUrl: '/static/partials/guide.html', controller: GuideCtrl}). + when('/tutorial/', {title: 'Tutorial', description:'Interactive tutorial for using Quay.io', templateUrl: '/static/partials/tutorial.html', + controller: TutorialCtrl}). when('/contact/', {title: 'Contact Us', description:'Different ways for you to get a hold of us when you need us most.', templateUrl: '/static/partials/contact.html', controller: ContactCtrl}). when('/plans/', {title: 'Plans and Pricing', description: 'Plans and pricing for private docker repositories on Quay.io', diff --git a/static/js/controllers.js b/static/js/controllers.js index 1ad325609..485d1e5b5 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -42,35 +42,62 @@ function PlansCtrl($scope, $location, UserService, PlanService) { }; } -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 GuideCtrl($scope) { +} + +function TutorialCtrl($scope, AngularTour, AngularTourSignals, UserService) { + $scope.tour = { + 'title': 'Quay.io Tutorial', + 'steps': [ + { + 'title': 'Welcome to the Quay.io tutorial!', + 'templateUrl': '/static/tutorial/welcome.html' + }, + { + 'title': 'Sign in to get started', + 'templateUrl': '/static/tutorial/signup.html', + 'signal': function($tourScope) { + $tourScope.username = UserService.currentUser().username; + $tourScope.email = UserService.currentUser().email; + return !UserService.currentUser().anonymous; } - ] - }); + }, + { + 'title': 'Step 1: Login to Quay.io', + 'templateUrl': '/static/tutorial/docker-login.html', + 'signal': AngularTourSignals.matchesLocation('/repository/'), + 'waitMessage': "Waiting for login" + }, + { + 'title': 'Step 2: Create a new image', + 'templateUrl': '/static/tutorial/create-image.html' + }, + { + 'title': 'Step 3: Push the image to Quay.io', + 'templateUrl': '/static/tutorial/push-image.html' + }, + { + 'title': 'Step 4: View the repository on Quay.io', + 'templateUrl': '/static/tutorial/view-repo.html', + 'signal': AngularTourSignals.matchesLocation('/repository/'), + 'waitMessage': "Waiting for image push to complete" + }, + { + 'content': 'Waiting for the page to load', + 'signal': AngularTourSignals.elementAvaliable('*[data-repo="{{username}}/{{repoName}}"]'), + 'overlayable': true + }, + { + 'content': 'Select your new repository from the list', + 'signal': AngularTourSignals.matchesLocation('/repository/{{username}}/{{repoName}}'), + 'element': '*[data-repo="{{username}}/{{repoName}}"]', + 'overlayable': true + }, + { + 'content': 'And done!', + 'overlayable': true + } + ] }; } diff --git a/static/js/tour.js b/static/js/tour.js index 4b26c60df..6ff358f9d 100644 --- a/static/js/tour.js +++ b/static/js/tour.js @@ -1,7 +1,11 @@ angular.module("angular-tour", []) .provider('AngularTour', function() { - this.$get = ['$document', '$rootScope', '$compile', function($document, $rootScope, $compile) { - function _start(tour) { + this.$get = ['$document', '$rootScope', '$compile', '$location', function($document, $rootScope, $compile, $location) { + $rootScope.angular_tour_current = null; + + function _start(tour, opt_stepIndex, opt_existingScope) { + tour.initialStep = opt_stepIndex || tour.initialStep || 0; + tour.tourScope = opt_existingScope || null; $rootScope.angular_tour_current = tour; } @@ -13,24 +17,53 @@ angular.module("angular-tour", []) start: _start, stop: _stop }; - }]; }) - .directive('angularTourOverlay', function() { + .directive('angularTourUi', function() { var directiveDefinitionObject = { priority: 0, - templateUrl: '/static/directives/angular-tour-overlay.html', + templateUrl: '/static/directives/angular-tour-ui.html', replace: false, transclude: false, restrict: 'C', scope: { - 'tour': '=tour' + 'tour': '=tour', + 'inline': '=inline', }, - controller: function($scope, $element, $interval) { + controller: function($rootScope, $scope, $element, $location, $interval, AngularTour) { + var createNewScope = function() { + var tourScope = { + '_replaceData': function(s) { + if (typeof s != 'string') { + return s; + } + + for (var key in tourScope) { + if (key[0] == '_') { continue; } + if (tourScope.hasOwnProperty(key)) { + s = s.replace('{{' + key + '}}', tourScope[key]); + } + } + return s; + } + }; + + return tourScope; + }; + $scope.stepIndex = 0; $scope.step = null; - $scope.interval = null; + $scope.interval = null; + $scope.tourScope = createNewScope(); + + var getElement = function() { + if (typeof $scope.step['element'] == 'function') { + return $($scope.step['element'](tourScope)); + } + + return $($scope.tourScope._replaceData($scope.step['element'])); + }; var checkSignalTimer = function() { if (!$scope.step) { @@ -38,8 +71,8 @@ angular.module("angular-tour", []) return; } - var signal = $scope.step.signal; - if (signal()) { + var signal = $scope.step['signal']; + if (signal($scope.tourScope)) { $scope.next(); } }; @@ -58,12 +91,12 @@ angular.module("angular-tour", []) var closeDomHighlight = function() { if (!$scope.step) { return; } - var element = $($scope.step.element); + var element = getElement($scope.tourScope); element.spotlight('close'); }; var updateDomHighlight = function() { - var element = $($scope.step.element); + var element = getElement(); if (!element.length) { return; } @@ -94,6 +127,13 @@ angular.module("angular-tour", []) } $scope.step = $scope.tour.steps[stepIndex]; + + // If the signal is already true, then skip this step entirely. + if ($scope.step['signal'] && $scope.step['signal']($scope.tourScope)) { + $scope.setStepIndex(stepIndex + 1); + return; + } + $scope.stepIndex = stepIndex; $scope.hasNextStep = stepIndex < $scope.tour.steps.length - 1; @@ -110,7 +150,9 @@ angular.module("angular-tour", []) }; $scope.stop = function() { + closeDomHighlight(); $scope.tour = null; + AngularTour.stop(); }; $scope.next = function() { @@ -119,8 +161,35 @@ angular.module("angular-tour", []) $scope.$watch('tour', function(tour) { stopSignalTimer(); - $scope.setStepIndex(0); + if (tour) { + $scope.setStepIndex(tour.initialStep || 0); + $scope.tourScope = tour.tourScope || createNewScope(); + $scope.tour.tourScope = $scope.tourScope; + } }); + + // If this is an inline tour, then we need to monitor the page to determine when + // to transition it to an overlay tour. + if ($scope.inline) { + var counter = 0; + var unbind = $rootScope.$watch(function() { + return $location.path(); + }, function(location) { + // Since this callback fires for the first page display, we only unbind it + // after the second call. + if (counter == 1) { + // Unbind the listener. + unbind(); + + // If there is an active tour, transition it over to the overlay. + if ($scope.tour && $scope.step && $scope.step['overlayable']) { + AngularTour.start($scope.tour, $scope.stepIndex, $scope.tourScope); + $scope.tour = null; + } + } + counter++; + }); + } } }; return directiveDefinitionObject; @@ -131,15 +200,15 @@ angular.module("angular-tour", []) // Signal: When the page location matches the given path. signals.matchesLocation = function(locationPath) { - return function() { - return $location.path() == locationPath; + return function(tourScope) { + return $location.path() == tourScope._replaceData(locationPath); }; }; // Signal: When an element is found in the page's DOM. signals.elementAvaliable = function(elementPath) { - return function() { - return $(elementPath).length > 0; + return function(tourScope) { + return $(tourScope._replaceData(elementPath)).length > 0; }; }; diff --git a/static/partials/tutorial.html b/static/partials/tutorial.html new file mode 100644 index 000000000..0ea653b2c --- /dev/null +++ b/static/partials/tutorial.html @@ -0,0 +1,3 @@ +
+
+
diff --git a/static/tutorial/docker-login.html b/static/tutorial/docker-login.html new file mode 100644 index 000000000..7427147c0 --- /dev/null +++ b/static/tutorial/docker-login.html @@ -0,0 +1,8 @@ +
+

The first step when using Quay.io is to login via the docker login command.

+

Enter your Quay.io username and your password when prompted.

+
docker login quay.io
+Username: {{ tour.tourScope.username }}
+Password: (password here)
+Email: {{ tour.tourScope.email }}
+
diff --git a/static/tutorial/signup.html b/static/tutorial/signup.html new file mode 100644 index 000000000..8aa174156 --- /dev/null +++ b/static/tutorial/signup.html @@ -0,0 +1,11 @@ +
+
+ This tutorial will interact with your account, so please sign in to get started +
+
+
+
+
+
+
+ diff --git a/static/tutorial/welcome.html b/static/tutorial/welcome.html new file mode 100644 index 000000000..9be990cd9 --- /dev/null +++ b/static/tutorial/welcome.html @@ -0,0 +1 @@ +This tutorial will walk you step-by-step through using Quay.io, including pushing/pulling repositories, making permissions changes and managing your repository. diff --git a/templates/base.html b/templates/base.html index ee09131ec..5ee39f371 100644 --- a/templates/base.html +++ b/templates/base.html @@ -171,6 +171,6 @@ var isProd = document.location.hostname === 'quay.io'; {% endif %} -
+
diff --git a/test/data/test.db b/test/data/test.db index be6291cdaf3680fcaa53c5ac81823dcc8be9fd92..332b0a5615ec84e24c824049cdb2ce34199a77f9 100644 GIT binary patch delta 536 zcmZvY-77CX-NP{6;ZLPdz<-pXd2J-=6r1Ieu-PuQjCzA&CWh8!Mc* z_0!=@m@YL~O-!XIuuKvt7*+>;K~bBn%r;S;H%rJ6Y#?Kp6}pCG{)w-UcnDNXZMRS@ z`L&v{gM7{BU>KsGuEN5DP=+So z=n%u^zeis2f(P8;3KuxTF%GbcJi{)-`nyxCl1Bxt#Bh6=;jIwuut{8uFoQc-!3_G) z1qTG~kvrtl+#DBU)NU@7?xYsK+o|~MsvdBA1B&AE_;g+ONHzqiTT#7E<)`fl_%xT- zt7(2!EVfg_jkLx%=%m_B)=~T*hjD^Uj5Br=Qz6Mh1VJDqSl3)eDmkROb(g|Iosfs3 qJux|8mwU%Xodc0@|JZ;$85DmYzA{LOoqr%hLS$rgFxC^5CqDrSGKiu8 delta 244 zcmZp8z|nAlV}dm6R0alyu89ivK!)xk&Bm0iDU5T}w`nl3uqv4V|ZsubQ%omxDG5=xy&isk_IrF3KQYMUxKnkZHuxDhM ze#)HDii4Z^AOlFnb^!~<<;aHEOK~%w0!slkzh!>G{FwP3^G)U}%;%X;Z5HHN!wfcL z^10t;P)9f*IpVN0qoWiz(;5cmN6d$rmoT?7r!l)TYcO*%y<$4ev<9fZk7;_I8za~B Pa!p2&?Wf!r%^m^(H?UF&