Get the basic tutorial working completely, including reacting to server-side events
This commit is contained in:
parent
b7afc83204
commit
fa1bf94af1
20 changed files with 431 additions and 99 deletions
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}]);
|
||||
|
|
Reference in a new issue