Get the basic tutorial working completely, including reacting to server-side events

This commit is contained in:
Joseph Schorr 2014-02-06 20:58:26 -05:00
parent b7afc83204
commit fa1bf94af1
20 changed files with 431 additions and 99 deletions

View file

@ -2836,7 +2836,7 @@ p.editable:hover i {
.angular-tour-ui-element.inline .step-title {
font-size: 20px;
margin-bottom: 10px;
margin-bottom: 20px;
}
.angular-tour-ui-element.inline .step-content {
@ -2850,7 +2850,7 @@ p.editable:hover i {
}
.angular-tour-ui-element p {
margin-bottom: 10px;
margin-bottom: 20px;
}
.angular-tour-ui-element .wait-message {
@ -2869,6 +2869,13 @@ p.editable:hover i {
border-left-color: #999;
}
.angular-tour-ui-element .note {
margin: 10px;
padding: 6px;
background: #eee;
border: 1px solid #ddd;
}
pre.command {
padding: 20px;
background: #fff;
@ -2876,13 +2883,19 @@ pre.command {
overflow: auto;
border: solid 1px #ccc;
position: relative;
margin-top: 20px;
margin-top: 10px;
margin-bottom: 20px;
}
pre.command:before {
content: "\f120";
font-family: "FontAwesome";
font-size: 16px;
margin-right: 6px;
color: #999;
content: "\f120";
font-family: "FontAwesome";
font-size: 16px;
margin-right: 6px;
color: #999;
}
.form-inline {
display: inline-block;
margin-left: 10px;
}

View file

@ -48,6 +48,10 @@ function GuideCtrl($scope) {
function TutorialCtrl($scope, AngularTour, AngularTourSignals, UserService) {
$scope.tour = {
'title': 'Quay.io Tutorial',
'initialScope': {
'repoName': 'myfirstrepo',
'containerId': 'containerId'
},
'steps': [
{
'title': 'Welcome to the Quay.io tutorial!',
@ -65,22 +69,47 @@ function TutorialCtrl($scope, AngularTour, AngularTourSignals, UserService) {
{
'title': 'Step 1: Login to Quay.io',
'templateUrl': '/static/tutorial/docker-login.html',
'signal': AngularTourSignals.matchesLocation('/repository/'),
'waitMessage': "Waiting for login"
'signal': AngularTourSignals.serverEvent('/realtime/user/subscribe?events=docker-cli',
function(message) {
return message['data']['action'] == 'login';
}),
'waitMessage': "Waiting for docker login"
},
{
'title': 'Step 2: Create a new image',
'title': 'Step 2: Create a new container',
'templateUrl': '/static/tutorial/create-container.html'
},
{
'title': 'Step 3: 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: Push the image to Quay.io',
'templateUrl': '/static/tutorial/push-image.html',
'signal': AngularTourSignals.serverEvent('/realtime/user/subscribe?events=docker-cli',
function(message, tourScope) {
var pushing = message['data']['action'] == 'push_repo';
if (pushing) {
tourScope.repoName = message['data']['repository'];
}
return pushing;
}),
'waitMessage': "Waiting for repository push to begin"
},
{
'title': 'Step 4: View the repository on Quay.io',
'title': 'Push in progress',
'templateUrl': '/static/tutorial/pushing.html',
'signal': AngularTourSignals.serverEvent('/realtime/user/subscribe?events=docker-cli',
function(message, tourScope) {
return message['data']['action'] == 'pushed_repo';
}),
'waitMessage': "Waiting for repository push to complete"
},
{
'title': 'Step 5: View the repository on Quay.io',
'templateUrl': '/static/tutorial/view-repo.html',
'signal': AngularTourSignals.matchesLocation('/repository/'),
'waitMessage': "Waiting for image push to complete"
'overlayable': true
},
{
'content': 'Waiting for the page to load',
@ -88,13 +117,61 @@ function TutorialCtrl($scope, AngularTour, AngularTourSignals, UserService) {
'overlayable': true
},
{
'content': 'Select your new repository from the list',
'templateUrl': '/static/tutorial/repo-list.html',
'signal': AngularTourSignals.matchesLocation('/repository/{{username}}/{{repoName}}'),
'element': '*[data-repo="{{username}}/{{repoName}}"]',
'overlayable': true
},
{
'content': 'And done!',
'title': 'Repository View',
'content': 'This is the repository view page. It displays all the primary information about your repository.',
'overlayable': true
},
{
'title': 'Image History',
'content': 'The tree displays the full history of your repository, including all its tag. ' +
'You can click on a tag or image to see its information.',
'element': '#image-history-container',
'overlayable': true
},
{
'title': 'Tag/Image Information',
'content': 'This panel displays information about the currently selected tag or image',
'element': '#side-panel',
'overlayable': true
},
{
'title': 'Select tag or image',
'content': 'You can select a tag or image by clicking on this dropdown',
'element': '#side-panel-dropdown',
'overlayable': true
},
{
'content': 'To view the admin settings for the repository, click on the gear',
'element': '#admin-cog',
'signal': AngularTourSignals.matchesLocation('/repository/{{username}}/{{repoName}}/admin'),
'overlayable': true
},
{
'title': 'Repository Admin',
'content': "The repository admin panel allows for modification of a repository's permissions, webhooks, visibility and other settings",
'overlayable': true
},
{
'title': 'Permissions',
'content': "The permissions tab displays all the users, robot accounts and tokens that have access to the repository",
'overlayable': true,
'element': '#permissions'
},
{
'title': 'Adding a permission',
'content': 'To add a permission, enter a username or robot account name into the autocomplete ' +
'or hit the dropdown arrow to manage robot accounts',
'overlayable': true,
'element': '#add-entity-permission'
},
{
'templateUrl': '/static/tutorial/done.html',
'overlayable': true
}
]
@ -679,9 +756,15 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
$scope.permissions = {'team': [], 'user': []};
$scope.logsShown = 0;
$scope.deleting = false;
$scope.permissionCache = {};
$scope.buildEntityForPermission = function(name, permission, kind) {
return {
var key = name + ':' + kind;
if ($scope.permissionCache[key]) {
return $scope.permissionCache[key];
}
return $scope.permissionCache[key] = {
'kind': kind,
'name': name,
'is_robot': permission.is_robot,
@ -702,7 +785,7 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
$scope.addNewPermission = function(entity) {
// Don't allow duplicates.
if ($scope.permissions[entity.kind][entity.name]) { return; }
if (!entity || !entity.kind || $scope.permissions[entity.kind][entity.name]) { return; }
if (entity.is_org_member === false) {
$scope.currentAddEntity = entity;
@ -1605,7 +1688,7 @@ function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
$rootScope.title = 'Loading...';
$scope.addNewMember = function(member) {
if ($scope.members[member.name]) { return; }
if (!member || $scope.members[member.name]) { return; }
var params = {
'orgname': orgname,

View file

@ -32,21 +32,20 @@ angular.module("angular-tour", [])
'inline': '=inline',
},
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]);
}
}
var createNewScope = function(initialScope) {
var tourScope = jQuery.extend({}, initialScope || {});
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;
@ -55,7 +54,7 @@ angular.module("angular-tour", [])
$scope.stepIndex = 0;
$scope.step = null;
$scope.interval = null;
$scope.tourScope = createNewScope();
$scope.tourScope = null;
var getElement = function() {
if (typeof $scope.step['element'] == 'function') {
@ -65,14 +64,35 @@ angular.module("angular-tour", [])
return $($scope.tourScope._replaceData($scope.step['element']));
};
var checkSignal = function() {
return $scope.step['signal'] && $scope.step['signal']($scope.tourScope);
};
var teardownSignal = function() {
if (!$scope.step) { return; }
var signal = $scope.step['signal'];
if (signal && signal.$teardown) {
signal.$teardown($scope.tourScope);
}
};
var setupSignal = function() {
if (!$scope.step) { return; }
var signal = $scope.step['signal'];
if (signal && signal.$setup) {
signal.$setup($scope.tourScope);
}
};
var checkSignalTimer = function() {
if (!$scope.step) {
if (!$scope.step || !$scope.tourScope) {
stopSignalTimer();
return;
}
var signal = $scope.step['signal'];
if (signal($scope.tourScope)) {
if (checkSignal()) {
$scope.next();
}
};
@ -116,6 +136,7 @@ angular.module("angular-tour", [])
$scope.setStepIndex = function(stepIndex) {
// Close existing spotlight and signal timer.
teardownSignal();
closeDomHighlight();
stopSignalTimer();
@ -129,7 +150,8 @@ 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)) {
setupSignal();
if (checkSignal()) {
$scope.setStepIndex(stepIndex + 1);
return;
}
@ -138,10 +160,11 @@ angular.module("angular-tour", [])
$scope.hasNextStep = stepIndex < $scope.tour.steps.length - 1;
// Need the timeout here to ensure the click event does not
// hide the spotlight.
// hide the spotlight, and it has to be longer than the hide
// spotlight animation timing.
setTimeout(function() {
updateDomHighlight();
}, 1);
}, 500);
// Start listening for signals to move the tour forward.
if ($scope.step.signal) {
@ -162,9 +185,15 @@ angular.module("angular-tour", [])
$scope.$watch('tour', function(tour) {
stopSignalTimer();
if (tour) {
// Set the tour scope.
if (tour.tourScope) {
$scope.tourScope = tour.tourScope;
} else {
$scope.tourScope = $scope.tour.tourScope = createNewScope(tour.initialScope);
}
// Set the initial step.
$scope.setStepIndex(tour.initialStep || 0);
$scope.tourScope = tour.tourScope || createNewScope();
$scope.tour.tourScope = $scope.tourScope;
}
});
@ -181,6 +210,9 @@ angular.module("angular-tour", [])
// Unbind the listener.
unbind();
// Teardown any existing signal listener.
teardownSignal();
// 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);
@ -212,5 +244,30 @@ angular.module("angular-tour", [])
};
};
// Signal: When a server-side event matches the predicate.
signals.serverEvent = function(url, matcher) {
var checker = function(tourScope) {
return checker.$message && matcher(checker.$message, tourScope);
};
checker.$message = null;
checker.$setup = function(tourScope) {
var fullUrl = tourScope._replaceData(url);
checker.$source = new EventSource(fullUrl);
checker.$source.onmessage = function(e) {
checker.$message = JSON.parse(e.data);
};
};
checker.$teardown = function() {
if (checker.$source) {
checker.$source.close();
}
};
return checker;
};
return signals;
}]);

View file

@ -25,7 +25,7 @@
<div class="plan-manager" organization="orgname" plan-changed="planChanged(plan)"></div>
</div>
<!-- Organiaztion settings tab -->
<!-- Organization settings tab -->
<div id="settings" class="tab-pane">
<div class="quay-spinner" ng-show="changingOrganization"></div>

View file

@ -89,7 +89,10 @@
<tr>
<td colspan="2" class="admin-search">
<span class="entity-search" namespace="repo.namespace" include-teams="true" input-title="'Add a ' + (repo.is_organization ? 'team or ' : '') + 'user...'" entity-selected="addNewPermission" is-organization="repo.is_organization"></span>
<span id="add-entity-permission" class="entity-search" namespace="repo.namespace" include-teams="true"
input-title="'Add a ' + (repo.is_organization ? 'team or ' : '') + 'user...'"
entity-selected="addNewPermission" is-organization="repo.is_organization"
current-entity="selectedEntity"></span>
</td>
</tr>
</table>

View file

@ -28,7 +28,10 @@
<div ng-show="user_repositories.value.length > 0">
<div class="repo-listing" ng-repeat="repository in user_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>

View file

@ -27,7 +27,8 @@
<tr ng-show="canEditMembers">
<td colspan="2">
<span class="entity-search" namespace="orgname" include-teams="false" input-title="'Add a user...'"
entity-selected="addNewMember" is-organization="true"></span>
entity-selected="addNewMember" is-organization="true"
current-entity="selectedMember"></span>
</td>
</tr>
</table>

View file

@ -12,7 +12,7 @@
<span class="repo-circle" repo="repo"></span>
<span class="repo-breadcrumb" repo="repo"></span>
<span class="settings-cog" ng-show="repo.can_admin" title="Repository Settings" bs-tooltip="tooltip.title" data-placement="bottom">
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/admin' }}">
<a id="admin-cog" href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/admin' }}">
<i class="fa fa-cog fa-lg"></i>
</a>
</span>
@ -88,10 +88,10 @@ sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
<!-- Side Panel -->
<div class="col-md-4">
<div class="panel panel-default">
<div id="side-panel" class="panel panel-default">
<div class="panel-heading">
<!-- Dropdown -->
<div class="tag-dropdown dropdown" data-placement="top">
<div id="side-panel-dropdown" class="tag-dropdown dropdown" data-placement="top">
<i class="fa fa-tag" ng-show="currentTag"></i>
<i class="fa fa-archive" ng-show="!currentTag"></i>
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">{{currentTag ? currentTag.name : currentImage.id.substr(0, 12)}} <b class="caret"></b></a>

View file

@ -0,0 +1,24 @@
<p>The first step to creating an image is to create a container and fill it with some data.</p>
<p>First we'll create a container with a single new file based off of the <code>ubuntu</code> base image:</p>
<pre class="command">
docker run ubuntu echo "fun" > newfile
</pre>
<p>The container will immediately terminate (because its one command is <code>echo</code>), so we'll use <code>docker ps -l</code> to list it:
<pre class="command">
docker ps -l
CONTAINER ID IMAGE COMMAND CREATED
<var class="var1">07f2065197ef</var> ubuntu:12.04 echo fun 31 seconds ago
</pre>
<div class="alert alert-info">
<div class="control-group">
<label class="control-label" for="containerId">Enter the command ID:</label>
<div class="form-inline">
<input type="text" id="containerId" class="form-control" ng-model="tour.tourScope.containerId">
</div>
</div>
</div>

View file

@ -0,0 +1,16 @@
<div class="alert alert-info">
<div class="control-group">
<label class="control-label" for="containerId">Enter a repo name:</label>
<div class="form-inline">
<input type="text" id="containerId" class="form-control" ng-model="tour.tourScope.repoName">
</div>
</div>
</div>
<p>Once a container has terminated in Docker, the next step is to <i>tag</i> the container to an image, so it can be saved to a repository.</p>
<p>To do so, we run the <code>docker commit</code> with the container ID from the previous step and tag it to be a repository under <code>quay.io</code>.
<pre class="command">
docker commit <var class="var1">{{ tour.tourScope.containerId }}</var> quay.io/{{ tour.tourScope.username }}/<var class="var2">{{ tour.tourScope.repoName }}</var>
</pre>

View file

@ -0,0 +1 @@
That's it for the introduction tutorial! If you have any questions, please check the <a href="http://docs.quay.io" target="_blank">Quay Documentation</a> or <a href="/contact">contact us</a>!

View file

@ -0,0 +1,8 @@
<p>Now that we've tagged our image with a repository name, we can <code>push</code> the repository to Quay.io:</p>
<pre class="command">
docker push quay.io/{{ tour.tourScope.username }}/<var class="var2">{{ tour.tourScope.repoName }}</var>
The push refers to a repository [quay.io/{{ tour.tourScope.username }}/<var class="var2">{{ tour.tourScope.repoName }}</var>] (len: 1)
Sending image list
Pushing repository quay.io/{{ tour.tourScope.username }}/<var class="var2">{{ tour.tourScope.repoName }}</var> (1 tags)
</pre>

View file

@ -0,0 +1 @@
Your repository {{ tour.tourScope.username }}/{{ tour.tourScope.repoName }} is currently being pushed to Quay.io...

View file

@ -0,0 +1,3 @@
<p>This page displays all your personal repositories, as well as select public repositories</p>
<p>To view your new repository, click on <strong>{{ tour.tourScope.username }}/{{ tour.tourScope.repoName }}</strong></p>

View file

@ -0,0 +1,3 @@
<p>Your repository {{ tour.tourScope.username }}/{{ tour.tourScope.repoName }} has been pushed to Quay.io!</p>
<p>To view your repository, first click on the <strong>Repositories</strong> link in the header to continue. This will display the repositories list.</p>