Start on tour infrastructure. Note that this code works but is NOT STYLED and has a FAKE TEMP TOUR in it

This commit is contained in:
Joseph Schorr 2014-02-04 20:50:13 -05:00
parent 37507b7d7d
commit a049fc57c6
10 changed files with 411 additions and 130 deletions

View file

@ -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;
}

View file

@ -0,0 +1,10 @@
<div class="angular-tour-overlay-element" ng-class="tour ? 'touring' : 'nottouring'">
<span class="tour-title">{{ tour.title }}</span>
<span class="step-title">{{ step.title }}</span>
<span class="step-content">{{ step.content }}</span>
<span class="controls">
<button ng-click="next()" ng-show="hasNextStep && !step.signal">Next</button>
<button ng-click="stop()" ng-show="!hasNextStep">Done</button>
</span>
</div>

View file

@ -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) {

View file

@ -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) {

147
static/js/tour.js Normal file
View file

@ -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;
}]);

View file

@ -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 = $('<canvas></canvas>');
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);

View file

@ -1,127 +1,4 @@
<div class="container content-container">
<div class="alert alert-warning">Warning: Quay requires docker version 0.6.2 or higher to work</div>
<h2>User Guide</h2>
<div class="user-guide container">
<h3>Signing into Quay <span class="label label-default">Setup</span></h3>
<div class="container">
To setup your Docker client for pushing to Quay, login with your credentials:
<br><br>
<pre>$ sudo docker login quay.io
Login against server at https://quay.io/v1/
Username: myusername
Password: mypassword
Email: my@email.com</pre>
</div>
<br>
<h3>Pushing a repository to Quay <span class="label label-success">Requires Write Access</span></h3>
<div class="container">
In order to push a repository to Quay, it must be <b>tagged</b> with the <b>quay.io</b> domain and the namespace under which it will live:
<br><br>
<pre>sudo docker tag <i>0u123imageid</i> quay.io/<i>username/repo_name</i></pre>
<br>
Once tagged, the repository can be pushed to Quay:<br><br>
<pre>sudo docker push quay.io/<i>username/repo_name</i></pre>
</div>
<br>
<h3>Pulling a repository from Quay</h3>
<div class="container">
<div class="alert alert-info">Note: <b>Private</b> repositories require you to be <b>logged in</b> or the pull will fail. See above for how to sign into Quay if you have never done so before. </div>
To pull a repository from Quay, run the following command:
<br><br>
<pre>sudo docker pull quay.io/<i>username/repo_name</i></pre>
</div>
<br>
<h3>Granting and managing permissions to users <span class="label label-info">Requires Admin Access</span></h3>
<div class="container">
<div class="description-overview">Quay allows a repository to be shared any number of users and to grant those users any level of permissions for a repository</div>
<ul class="description-list">
<li>Permissions for a repository can be granted and managed in the repository's admin interface
<li><b>Adding a user:</b> Type that user's username in the "Add New User..." field, and select the user
<li><b>Changing permissions:</b> A user's permissions (read, read/write or admin) can be changed by clicking the field to the right of the user
<li><b>Removing a user:</b> A user can be removed from the list by clicking the <b>X</b> and then clicking <b>Delete</b>
</ul>
</div>
<br>
<h3>Using robot accounts <span class="label label-info">Requires Admin Access</span></h3>
<div class="container">
<div class="description-overview">
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 <b>robot accounts</b> 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.
</div>
<ul class="description-list">
<li>Robot accounts can be managed in the user or organization admin's interface
<li><b>Adding a robot account:</b> Click "Create Robot Account" and enter a name for the account. The username will become <b>namespace+accountname</b> where "namespace" is the name of the user or organiaztion.
<li><b>Setting permissions:</b> Permissions can be granted to a robot account in a repository by adding that account like any other user or team.
<li><b>Deleting a robot account:</b> A robot account can be deleted by clicking the <b>X</b> and then clicking <b>Delete</b>
<li><b>Using a robot account:</b> To use the robot account, the following credentials can be used:
<dl class="dl-horizontal">
<dt>Username</dt><dd>namespace+accountname (Example: mycompany+deploy)</dd>
<dt>Password</dt><dd>(token value can be found by clicking on the robot account in the admin panel)</dd>
<dt>Email</dt><dd>This value is ignored, any value may be used.</dd>
</dl>
</ul>
</div>
<h3>Using access tokens in place of users <span class="label label-info">Requires Admin Access</span></h3>
<div class="container">
<div class="description-overview">
For per-repository token authentication, Quay allows the use of <b>access tokens</b> which can be created on a repository and have read and/or write
permissions, without any passwords.
</div>
<ul class="description-list">
<li>Tokens can be managed in the repository's admin interface
<li><b>Adding a token:</b> Enter a user-readable description in the "New token description" field
<li><b>Changing permissions:</b> A token's permissions (read or read/write) can be changed by clicking the field to the right of the token
<li><b>Deleting a token:</b> A token can be deleted by clicking the <b>X</b> and then clicking <b>Delete</b>
<li><b>Using a token:</b> To use the token, the following credentials can be used:
<dl class="dl-horizontal">
<dt>Username</dt><dd>$token</dd>
<dt>Password</dt><dd>(token value can be found by clicking on the token)</dd>
<dt>Email</dt><dd>This value is ignored, any value may be used.</dd>
</dl>
</ul>
</div>
<h3>Deleting a tag <span class="label label-info">Requires Admin Access</span></h3>
<div class="container">
<div class="description-overview">
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 <b>unique to it</b>. Images will not be deleted until all tags sharing them are deleted.
</div>
</div>
<a name="#post-hook"></a>
<h3>Using push webhooks <span class="label label-info">Requires Admin Access</span></h3>
<div class="container">
A repository can have one or more <b>push webhooks</b> setup, which will be invoked whenever <u>a successful push occurs</u>. Webhooks can be managed from the repository's admin interface.
<br><br> A webhook will be invoked
as an HTTP <b>POST</b> to the specified URL, with a JSON body describing the push:<br><br>
<pre>
{
<span class="context-tooltip" title="The number of images pushed" bs-tooltip="tooltip.title">"pushed_image_count"</span>: 2,
<span class="context-tooltip" title="The name of the repository (without its namespace)" bs-tooltip="tooltip.title">"name"</span>: "ubuntu",
<span class="context-tooltip" title="The full name of the repository" bs-tooltip="tooltip.title">"repository"</span>:"devtable/ubuntu",
<span class="context-tooltip" title="The URL at which the repository can be pulled by Docker" bs-tooltip="tooltip.title">"docker_url"</span>: "quay.io/devtable/ubuntu",
<span class="context-tooltip" title="Map of updated tag names to their latest image IDs" bs-tooltip="tooltip.title">"updated_tags"</span>: {
"latest": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc"
},
<span class="context-tooltip" title="The namespace of the repository" bs-tooltip="tooltip.title">"namespace"</span>: "devtable",
<span class="context-tooltip" title="Whether the repository is public or private" bs-tooltip="tooltip.title">"visibility"</span>: "private",
<span class="context-tooltip" title="The Quay URL for the repository" bs-tooltip="tooltip.title">"homepage"</span>: "https://quay.io/repository/devtable/ubuntu"
}
</pre>
</div>
</div>
<button ng-click="startTour()">Start Tour</button>
<div id="test-element">
This is a test element
</div>

View file

@ -50,7 +50,10 @@
<div class="resource-view" resource="public_repositories">
<div class="repo-listing" ng-repeat="repository in public_repositories.value">
<span class="repo-circle no-background" repo="repository"></span>
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}"
data-repo="{{repository.namespace}}/{{ repository.name }}">
{{repository.namespace}}/{{repository.name}}
</a>
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
</div>
<div class="page-controls">

View file

@ -72,6 +72,7 @@
window.__token = '{{ csrf_token() }}';
</script>
<script src="static/js/tour.js"></script>
<script src="static/js/app.js"></script>
<script src="static/js/controllers.js"></script>
<script src="static/js/graphing.js"></script>
@ -170,5 +171,6 @@ var isProd = document.location.hostname === 'quay.io';
{% endif %}
<!-- end olark code -->
<div class="angular-tour-overlay" tour="angular_tour_current"></div>
</body>
</html>

View file

@ -35,6 +35,7 @@
<script src="static/lib/Blob.js"></script>
<script src="static/lib/FileSaver.js"></script>
<script src="static/lib/jquery.base64.min.js"></script>
<script src="static/lib/jquery.spotlight.js"></script>
{% endblock %}
{% block body_content %}