Robot accounts allow for delegating access in multiple repositories to role-based accounts that you manage
diff --git a/static/directives/signup-form.html b/static/directives/signup-form.html
index b72c7636e..562223d96 100644
--- a/static/directives/signup-form.html
+++ b/static/directives/signup-form.html
@@ -19,7 +19,7 @@
diff --git a/static/directives/spinner.html b/static/directives/spinner.html
new file mode 100644
index 000000000..c0e0eb0e9
--- /dev/null
+++ b/static/directives/spinner.html
@@ -0,0 +1 @@
+
diff --git a/static/js/app.js b/static/js/app.js
index 8c8dc1610..09a49eb51 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -89,7 +89,9 @@ function getMarkedDown(string) {
}
// Start the application code itself.
-quayApp = angular.module('quay', ['ngRoute', 'restangular', 'angularMoment', 'angulartics', 'angulartics.google.analytics', 'angulartics.mixpanel', '$strap.directives', 'ngCookies'], function($provide) {
+quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'restangular', 'angularMoment', 'angulartics', 'angulartics.google.analytics', 'angulartics.mixpanel', '$strap.directives', 'ngCookies'], function($provide, cfpLoadingBarProvider) {
+ cfpLoadingBarProvider.includeSpinner = false;
+
$provide.factory('CookieService', ['$cookies', '$cookieStore', function($cookies, $cookieStore) {
var cookieService = {};
cookieService.putPermanent = function(name, value) {
@@ -127,6 +129,15 @@ quayApp = angular.module('quay', ['ngRoute', 'restangular', 'angularMoment', 'an
return CookieService.get('quay.loggedin') == 'true';
};
+ userService.updateUserIn = function(scope, opt_callback) {
+ scope.$watch(function () { return userService.currentUser(); }, function (currentUser) {
+ scope.user = currentUser;
+ if (opt_callback) {
+ opt_callback(currentUser);
+ }
+ }, true);
+ };
+
userService.load = function(opt_callback) {
var userFetch = Restangular.one('user/');
userFetch.get().then(function(loadedUser) {
@@ -187,6 +198,48 @@ quayApp = angular.module('quay', ['ngRoute', 'restangular', 'angularMoment', 'an
return userService;
}]);
+
+ $provide.factory('ApiService', ['Restangular', function(Restangular) {
+ var apiService = {}
+ apiService.at = function(locationPieces) {
+ var location = getRestUrl.apply(this, arguments);
+ var info = {
+ 'url': location,
+ 'caller': Restangular.one(location),
+ 'withOptions': function(options) {
+ info.options = options;
+ return info;
+ },
+ 'get': function(processor, opt_errorHandler) {
+ var options = info.options;
+ var caller = info.caller;
+ var result = {
+ 'loading': true,
+ 'value': null,
+ 'hasError': false
+ };
+
+ caller.get(options).then(function(resp) {
+ result.value = processor(resp);
+ result.loading = false;
+ }, function(resp) {
+ result.hasError = true;
+ result.loading = false;
+ if (opt_errorHandler) {
+ opt_errorHandler(resp);
+ }
+ });
+
+ return result;
+ }
+ };
+
+ return info;
+ };
+
+ return apiService;
+ }]);
+
$provide.factory('KeyService', ['$location', function($location) {
var keyService = {}
@@ -554,11 +607,11 @@ quayApp = angular.module('quay', ['ngRoute', 'restangular', 'angularMoment', 'an
// WARNING WARNING WARNING
$routeProvider.
when('/repository/:namespace/:name', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl,
- fixFooter: true, reloadOnSearch: false}).
+ fixFooter: false, reloadOnSearch: false}).
when('/repository/:namespace/:name/tag/:tag', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl,
- fixFooter: true}).
- when('/repository/:namespace/:name/image/:image', {templateUrl: '/static/partials/image-view.html', controller: ImageViewCtrl}).
- when('/repository/:namespace/:name/admin', {templateUrl: '/static/partials/repo-admin.html', controller:RepoAdminCtrl, reloadOnSearch: false}).
+ fixFooter: false}).
+ when('/repository/:namespace/:name/image/:image', {templateUrl: '/static/partials/image-view.html', controller: ImageViewCtrl, reloadOnSearch: false}).
+ when('/repository/:namespace/:name/admin', {templateUrl: '/static/partials/repo-admin.html', controller:RepoAdminCtrl, reloadOnSearch: false}).
when('/repository/', {title: 'Repositories', description: 'Public and private docker repositories list',
templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}).
when('/user/', {title: 'Account Settings', description:'Account settings for Quay.io', templateUrl: '/static/partials/user-admin.html',
@@ -1266,6 +1319,39 @@ quayApp.directive('popupInputButton', function () {
});
+quayApp.directive('resourceView', function () {
+ var directiveDefinitionObject = {
+ priority: 0,
+ templateUrl: '/static/directives/resource-view.html',
+ replace: false,
+ transclude: true,
+ restrict: 'C',
+ scope: {
+ 'resource': '=resource',
+ 'errorMessage': '=errorMessage'
+ },
+ controller: function($scope, $element) {
+ }
+ };
+ return directiveDefinitionObject;
+});
+
+
+quayApp.directive('quaySpinner', function () {
+ var directiveDefinitionObject = {
+ priority: 0,
+ templateUrl: '/static/directives/spinner.html',
+ replace: false,
+ transclude: true,
+ restrict: 'C',
+ scope: {},
+ controller: function($scope, $element) {
+ }
+ };
+ return directiveDefinitionObject;
+});
+
+
quayApp.directive('organizationHeader', function () {
var directiveDefinitionObject = {
priority: 0,
@@ -1529,18 +1615,18 @@ quayApp.directive('entitySearch', function () {
number++;
var input = $element[0].firstChild.firstChild;
- $scope.namespace = $scope.namespace || '';
$(input).typeahead({
name: 'entities' + number,
remote: {
url: '/api/entities/%QUERY',
replace: function (url, uriEncodedQuery) {
- url = url.replace('%QUERY', uriEncodedQuery);
- url += '?namespace=' + encodeURIComponent($scope.namespace);
- if ($scope.includeTeams) {
- url += '&includeTeams=true'
- }
- return url;
+ var namespace = $scope.namespace || '';
+ url = url.replace('%QUERY', uriEncodedQuery);
+ url += '?namespace=' + encodeURIComponent(namespace);
+ if ($scope.includeTeams) {
+ url += '&includeTeams=true'
+ }
+ return url;
},
filter: function(data) {
var datums = [];
@@ -1566,8 +1652,8 @@ quayApp.directive('entitySearch', function () {
}
template += '
' + datum.value + ' ';
- if (datum.entity.is_org_member !== undefined && !datum.entity.is_org_member && datum.kind == 'user') {
- template += '
This user is outside your organization
';
+ if (datum.entity.is_org_member === false && datum.entity.kind == 'user') {
+ template += '
';
}
template += '
';
diff --git a/static/js/controllers.js b/static/js/controllers.js
index 196668b6f..790355d80 100644
--- a/static/js/controllers.js
+++ b/static/js/controllers.js
@@ -15,7 +15,7 @@ $.fn.clipboardCopy = function() {
});
};
-function SigninCtrl($scope, $location, $timeout, Restangular, KeyService, UserService) {
+function SigninCtrl($scope) {
};
function PlansCtrl($scope, $location, UserService, PlanService) {
@@ -24,17 +24,13 @@ function PlansCtrl($scope, $location, UserService, PlanService) {
$scope.plans = plans;
});
- $scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
- $scope.user = currentUser;
- }, true);
+ // Monitor any user changes and place the current user into the scope.
+ UserService.updateUserIn($scope);
$scope.signedIn = function() {
$('#signinModal').modal('hide');
PlanService.handleNotedPlan();
};
-
- $scope.cancelNotedPlan = function() {
- };
$scope.buyNow = function(plan) {
if ($scope.user && !$scope.user.anonymous) {
@@ -61,61 +57,50 @@ function GuideCtrl($scope) {
function SecurityCtrl($scope) {
}
-function RepoListCtrl($scope, Restangular, UserService) {
+function RepoListCtrl($scope, Restangular, UserService, ApiService) {
$scope.namespace = null;
+
+ // Monitor changes in the user.
+ UserService.updateUserIn($scope, function() {
+ loadMyRepos($scope.namespace);
+ });
- $scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
- $scope.user = currentUser;
- }, true);
-
+ // Monitor changes in the namespace.
$scope.$watch('namespace', function(namespace) {
loadMyRepos(namespace);
});
- $scope.loading = true;
- $scope.public_repositories = null;
- $scope.user_repositories = [];
-
var loadMyRepos = function(namespace) {
if (!$scope.user || $scope.user.anonymous || !namespace) {
return;
}
- $scope.loadingmyrepos = true;
-
- // Load the list of repositories.
- var params = {
- 'public': false,
- 'sort': true,
- 'namespace': namespace
- };
-
- var repositoryFetch = Restangular.all('repository/');
- repositoryFetch.getList(params).then(function(resp) {
- $scope.user_repositories = resp.repositories;
- $scope.loading = !($scope.public_repositories && $scope.user_repositories);
+ var options = {'public': false, 'sort': true, 'namespace': namespace};
+ $scope.user_repositories = ApiService.at('repository').withOptions(options).get(function(resp) {
+ return resp.repositories;
});
};
- // Load the list of public repositories.
- var options = {'public': true, 'private': false, 'sort': true, 'limit': 10};
- var repositoryPublicFetch = Restangular.all('repository/');
- repositoryPublicFetch.getList(options).then(function(resp) {
- $scope.public_repositories = resp.repositories;
- $scope.loading = !($scope.public_repositories && $scope.user_repositories);
- });
+ var loadPublicRepos = function() {
+ var options = {'public': true, 'private': false, 'sort': true, 'limit': 10};
+ $scope.public_repositories = ApiService.at('repository').withOptions(options).get(function(resp) {
+ return resp.repositories;
+ });
+ };
+
+ loadPublicRepos();
}
-function LandingCtrl($scope, $timeout, $location, Restangular, UserService, KeyService, PlanService) {
+function LandingCtrl($scope, UserService, ApiService) {
$scope.namespace = null;
$scope.$watch('namespace', function(namespace) {
loadMyRepos(namespace);
});
- $scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
- $scope.user = currentUser;
- }, true);
+ UserService.updateUserIn($scope, function() {
+ loadMyRepos($scope.namespace);
+ });
$scope.canCreateRepo = function(namespace) {
if (!$scope.user) { return false; }
@@ -141,40 +126,44 @@ function LandingCtrl($scope, $timeout, $location, Restangular, UserService, KeyS
return;
}
- $scope.loadingmyrepos = true;
-
- // Load the list of repositories.
- var params = {
- 'limit': 4,
- 'public': false,
- 'sort': true,
- 'namespace': namespace
- };
-
- var repositoryFetch = Restangular.all('repository/');
- repositoryFetch.getList(params).then(function(resp) {
- $scope.myrepos = resp.repositories;
- $scope.loadingmyrepos = false;
+ var options = {'limit': 4, 'public': false, 'sort': true, 'namespace': namespace };
+ $scope.my_repositories = ApiService.at('repository').withOptions(options).get(function(resp) {
+ return resp.repositories;
});
};
browserchrome.update();
}
-function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location, $timeout) {
+function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $timeout) {
+ var namespace = $routeParams.namespace;
+ var name = $routeParams.name;
+
$rootScope.title = 'Loading...';
+ // Watch for the destruction of the scope.
$scope.$on('$destroy', function() {
if ($scope.tree) {
$scope.tree.dispose();
}
});
+ // Watch for changes to the repository.
+ $scope.$watch('repo', function() {
+ if ($scope.tree) {
+ $timeout(function() {
+ $scope.tree.notifyResized();
+ });
+ }
+ });
+
// Watch for changes to the tag parameter.
$scope.$on('$routeUpdate', function(){
$scope.setTag($location.search().tag, false);
});
+ // Start scope methods //////////////////////////////////////////
+
$scope.updateForDescription = function(content) {
$scope.repo.description = content;
$scope.repo.put();
@@ -188,135 +177,9 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location, $tim
return moment($scope.parseDate(createdTime)).fromNow();
};
- var getDefaultTag = function() {
- if ($scope.repo === undefined) {
- return undefined;
- } else if ($scope.repo.tags.hasOwnProperty('latest')) {
- return $scope.repo.tags['latest'];
- } else {
- for (key in $scope.repo.tags) {
- return $scope.repo.tags[key];
- }
- }
- };
-
- $scope.$watch('repo', function() {
- if ($scope.tree) {
- $timeout(function() {
- $scope.tree.notifyResized();
- });
- }
- });
-
- var fetchRepository = function() {
- var repositoryFetch = Restangular.one('repository/' + namespace + '/' + name);
- repositoryFetch.get().then(function(repo) {
- $rootScope.title = namespace + '/' + name;
-
- var kind = repo.is_public ? 'public' : 'private';
- $rootScope.description = jQuery(getFirstTextLine(repo.description)).text() ||
- 'View of a ' + kind + ' docker repository on Quay';
-
- $scope.repo = repo;
-
- $scope.setTag($routeParams.tag);
-
- $('#copyClipboard').clipboardCopy();
- $scope.loading = false;
-
- if (repo.is_building) {
- startBuildInfoTimer(repo);
- }
- }, function() {
- $scope.repo = null;
- $scope.loading = false;
- $rootScope.title = 'Unknown Repository';
- });
- };
-
- var startBuildInfoTimer = function(repo) {
- if ($scope.interval) { return; }
-
- getBuildInfo(repo);
- $scope.interval = setInterval(function() {
- $scope.$apply(function() { getBuildInfo(repo); });
- }, 5000);
-
- $scope.$on("$destroy", function() {
- cancelBuildInfoTimer();
- });
- };
-
- var cancelBuildInfoTimer = function() {
- if ($scope.interval) {
- clearInterval($scope.interval);
- }
- };
-
- var getBuildInfo = function(repo) {
- var buildInfo = Restangular.one('repository/' + repo.namespace + '/' + repo.name + '/build/');
- buildInfo.get().then(function(resp) {
- var runningBuilds = [];
- for (var i = 0; i < resp.builds.length; ++i) {
- var build = resp.builds[i];
- if (build.status != 'complete') {
- runningBuilds.push(build);
- }
- }
-
- $scope.buildsInfo = runningBuilds;
- if (!runningBuilds.length) {
- // Cancel the build timer.
- cancelBuildInfoTimer();
-
- // Mark the repo as no longer building.
- $scope.repo.is_building = false;
-
- // Reload the repo information.
- fetchRepository();
- listImages();
- }
- });
- };
-
- var listImages = function() {
- var imageFetch = Restangular.one('repository/' + namespace + '/' + name + '/image/');
- imageFetch.get().then(function(resp) {
- $scope.imageHistory = resp.images;
-
- // Dispose of any existing tree.
- if ($scope.tree) {
- $scope.tree.dispose();
- }
-
- // Create the new tree.
- $scope.tree = new ImageHistoryTree(namespace, name, resp.images,
- getFirstTextLine, $scope.getTimeSince);
-
- $scope.tree.draw('image-history-container');
-
- // If we already have a tag, use it
- if ($scope.currentTag) {
- $scope.tree.setTag($scope.currentTag.name);
- }
-
- $($scope.tree).bind('tagChanged', function(e) {
- $scope.$apply(function() { $scope.setTag(e.tag, true); });
- });
- $($scope.tree).bind('imageChanged', function(e) {
- $scope.$apply(function() { $scope.setImage(e.image); });
- });
- });
- };
-
$scope.loadImageChanges = function(image) {
- $scope.currentImageChanges = null;
-
- var changesFetch = Restangular.one('repository/' + namespace + '/' + name + '/image/' + image.id + '/changes');
- changesFetch.get().then(function(changeInfo) {
- $scope.currentImageChanges = changeInfo;
- }, function() {
- $scope.currentImageChanges = {'added': [], 'removed': [], 'changed': []};
+ $scope.currentImageChangeResource = ApiService.at('repository', namespace, name, 'image', image.id, 'changes').get(function(ci) {
+ $scope.currentImageChanges = ci;
});
};
@@ -373,22 +236,134 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location, $tim
return count;
};
- var namespace = $routeParams.namespace;
- var name = $routeParams.name;
+ var getDefaultTag = function() {
+ if ($scope.repo === undefined) {
+ return undefined;
+ } else if ($scope.repo.tags.hasOwnProperty('latest')) {
+ return $scope.repo.tags['latest'];
+ } else {
+ for (key in $scope.repo.tags) {
+ return $scope.repo.tags[key];
+ }
+ }
+ };
- $scope.loading = true;
+ var fetchRepository = function() {
+ $rootScope.title = 'Loading Repository...';
+ $scope.repository = ApiService.at('repository', namespace, name).get(function(repo) {
+ // Set the repository object.
+ $scope.repo = repo;
- // Fetch the repo.
- fetchRepository();
+ // Set the default tag.
+ $scope.setTag($routeParams.tag);
- // Fetch the image history.
- listImages();
+ // Set the title of the page.
+ $rootScope.title = namespace + '/' + name;
+ var kind = repo.is_public ? 'public' : 'private';
+ $rootScope.description = jQuery(getFirstTextLine(repo.description)).text() ||
+ 'View of a ' + kind + ' docker repository on Quay';
+
+ // If the repository is marked as building, start monitoring it for changes.
+ if (repo.is_building) {
+ startBuildInfoTimer(repo);
+ }
+
+ $('#copyClipboard').clipboardCopy();
+ });
+ };
+
+ var startBuildInfoTimer = function(repo) {
+ if ($scope.interval) { return; }
+
+ getBuildInfo(repo);
+ $scope.interval = setInterval(function() {
+ $scope.$apply(function() { getBuildInfo(repo); });
+ }, 5000);
+
+ $scope.$on("$destroy", function() {
+ cancelBuildInfoTimer();
+ });
+ };
+
+ var cancelBuildInfoTimer = function() {
+ if ($scope.interval) {
+ clearInterval($scope.interval);
+ }
+ };
+
+ var getBuildInfo = function(repo) {
+ var buildInfo = Restangular.one('repository/' + repo.namespace + '/' + repo.name + '/build/');
+ buildInfo.withHttpConfig({
+ 'ignoreLoadingBar': true
+ });
+
+ buildInfo.get().then(function(resp) {
+ var runningBuilds = [];
+ for (var i = 0; i < resp.builds.length; ++i) {
+ var build = resp.builds[i];
+ if (build.status != 'complete') {
+ runningBuilds.push(build);
+ }
+ }
+
+ $scope.buildsInfo = runningBuilds;
+ if (!runningBuilds.length) {
+ // Cancel the build timer.
+ cancelBuildInfoTimer();
+
+ // Mark the repo as no longer building.
+ $scope.repo.is_building = false;
+
+ // Reload the repo information.
+ loadViewInfo();
+ }
+ });
+ };
+
+ var listImages = function() {
+ $scope.imageHistory = ApiService.at('repository', namespace, name, 'image').get(function(resp) {
+ // Dispose of any existing tree.
+ if ($scope.tree) {
+ $scope.tree.dispose();
+ }
+
+ // Create the new tree.
+ $scope.tree = new ImageHistoryTree(namespace, name, resp.images,
+ getFirstTextLine, $scope.getTimeSince);
+
+ $scope.tree.draw('image-history-container');
+
+ // If we already have a tag, use it
+ if ($scope.currentTag) {
+ $scope.tree.setTag($scope.currentTag.name);
+ }
+
+ // Listen for changes to the selected tag and image in the tree.
+ $($scope.tree).bind('tagChanged', function(e) {
+ $scope.$apply(function() { $scope.setTag(e.tag, true); });
+ });
+
+ $($scope.tree).bind('imageChanged', function(e) {
+ $scope.$apply(function() { $scope.setImage(e.image); });
+ });
+
+ return resp.images;
+ });
+ };
+
+ var loadViewInfo = function() {
+ fetchRepository();
+ listImages();
+ };
+
+ // Fetch the repository itself as well as the image history.
+ loadViewInfo();
}
-function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
+function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope) {
$('.info-icon').popover({
- 'trigger': 'hover',
- 'html': true
+ 'trigger': 'hover',
+ 'html': true
});
var namespace = $routeParams.namespace;
@@ -547,68 +522,24 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
});
};
- $scope.loading = true;
-
- var checkLoading = function() {
- $scope.loading = !($scope.permissions['user'] && $scope.permissions['team'] && $scope.repo && $scope.tokens);
- };
-
- var fetchPermissions = function(kind) {
- var permissionsFetch = Restangular.one('repository/' + namespace + '/' + name + '/permissions/' + kind + '/');
- permissionsFetch.get().then(function(resp) {
- $rootScope.title = 'Settings - ' + namespace + '/' + name;
- $rootScope.description = 'Administrator settings for ' + namespace + '/' + name +
- ': Permissions, webhooks and other settings';
- $scope.permissions[kind] = resp.permissions;
- checkLoading();
- }, function() {
- $scope.permissions[kind] = null;
- $rootScope.title = 'Unknown Repository';
- $scope.loading = false;
- });
- };
-
- // Fetch the repository information.
- var repositoryFetch = Restangular.one('repository/' + namespace + '/' + name);
- repositoryFetch.get().then(function(repo) {
- $scope.repo = repo;
- $scope.loading = !($scope.permissions && $scope.repo && $scope.tokens);
- }, function() {
- $scope.permissions = null;
- $rootScope.title = 'Unknown Repository';
- $scope.loading = false;
- });
-
- // Fetch the user and team permissions.
- fetchPermissions('user');
- fetchPermissions('team');
-
- // Fetch the tokens.
- var tokensFetch = Restangular.one('repository/' + namespace + '/' + name + '/tokens/');
- tokensFetch.get().then(function(resp) {
- $scope.tokens = resp.tokens;
- checkLoading();
- }, function() {
- $scope.tokens = null;
- $scope.loading = false;
- });
-
- $scope.webhooksLoading = true;
$scope.loadWebhooks = function() {
- $scope.webhooksLoading = true;
- var fetchWebhooks = Restangular.one('repository/' + namespace + '/' + name + '/webhook/');
- fetchWebhooks.get().then(function(resp) {
+ $scope.newWebhook = {};
+ $scope.webhooksResource = ApiService.at('repository', namespace, name, 'webhook').get(function(resp) {
$scope.webhooks = resp.webhooks;
- $scope.webhooksLoading = false;
+ return $scope.webhooks;
});
};
$scope.createWebhook = function() {
+ if (!$scope.newWebhook.url) {
+ return;
+ }
+
var newWebhook = Restangular.one('repository/' + namespace + '/' + name + '/webhook/');
newWebhook.customPOST($scope.newWebhook).then(function(resp) {
$scope.webhooks.push(resp);
$scope.newWebhook.url = '';
- $scope.newWebhookForm.$setPristine();
+ $scope.createWebhookForm.$setPristine();
});
};
@@ -618,26 +549,60 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
$scope.webhooks.splice($scope.webhooks.indexOf(webhook), 1);
});
};
+
+ var fetchTokens = function() {
+ var tokensFetch = Restangular.one('repository/' + namespace + '/' + name + '/tokens/');
+ tokensFetch.get().then(function(resp) {
+ $scope.tokens = resp.tokens;
+ }, function() {
+ $scope.tokens = null;
+ });
+ };
+
+ var fetchPermissions = function(kind) {
+ var permissionsFetch = Restangular.one('repository/' + namespace + '/' + name + '/permissions/' + kind + '/');
+ permissionsFetch.get().then(function(resp) {
+ $scope.permissions[kind] = resp.permissions;
+ }, function() {
+ $scope.permissions[kind] = null;
+ });
+ };
+
+ var fetchRepository = function() {
+ $scope.repository = ApiService.at('repository', namespace, name).get(function(repo) {
+ $scope.repo = repo;
+
+ $rootScope.title = 'Settings - ' + namespace + '/' + name;
+ $rootScope.description = 'Administrator settings for ' + namespace + '/' + name +
+ ': Permissions, webhooks and other settings';
+
+ // Fetch all the permissions and token info for the repository.
+ fetchPermissions('user');
+ fetchPermissions('team');
+ fetchTokens();
+
+ return $scope.repo;
+ });
+ };
+
+ // Fetch the repository.
+ fetchRepository();
}
function UserAdminCtrl($scope, $timeout, $location, Restangular, PlanService, UserService, KeyService, $routeParams) {
- $scope.$watch(function () { return UserService.currentUser(); }, function (currentUser) {
- $scope.askForPassword = currentUser.askForPassword;
- if (!currentUser.anonymous) {
- $scope.user = currentUser;
- }
- $scope.loading = false;
- }, true);
+ if ($routeParams['migrate']) {
+ $('#migrateTab').tab('show')
+ }
+
+ UserService.updateUserIn($scope, function(user) {
+ $scope.askForPassword = user.askForPassword;
+ });
$scope.readyForPlan = function() {
// Show the subscribe dialog if a plan was requested.
return $routeParams['plan'];
};
- if ($routeParams['migrate']) {
- $('#migrateTab').tab('show')
- }
-
$scope.loading = true;
$scope.updatingUser = false;
$scope.changePasswordSuccess = false;
@@ -716,13 +681,11 @@ function UserAdminCtrl($scope, $timeout, $location, Restangular, PlanService, Us
};
}
-function ImageViewCtrl($scope, $routeParams, $rootScope, Restangular) {
+function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, Restangular) {
var namespace = $routeParams.namespace;
var name = $routeParams.name;
var imageid = $routeParams.image;
- $('#copyClipboard').clipboardCopy();
-
$scope.parseDate = function(dateString) {
return Date.parse(dateString);
};
@@ -768,60 +731,63 @@ function ImageViewCtrl($scope, $routeParams, $rootScope, Restangular) {
if ($scope.tree) { return; }
$scope.tree = new ImageFileChangeTree($scope.image, $scope.combinedChanges);
- setTimeout(function() {
+ $timeout(function() {
$scope.tree.draw('changes-tree-container');
}, 10);
};
- // Fetch the image.
- var imageFetch = Restangular.one('repository/' + namespace + '/' + name + '/image/' + imageid);
- imageFetch.get().then(function(image) {
- $scope.loading = false;
- $scope.repo = {
- 'name': name,
- 'namespace': namespace
- };
- $scope.image = image;
- $rootScope.title = 'View Image - ' + image.id;
- $rootScope.description = 'Viewing docker image ' + image.id + ' under repository ' + namespace + '/' + name +
+ var fetchImage = function() {
+ $scope.image = ApiService.at('repository', namespace, name, 'image', imageid).get(function(image) {
+ $scope.repo = {
+ 'name': name,
+ 'namespace': namespace
+ };
+
+ $rootScope.title = 'View Image - ' + image.id;
+ $rootScope.description = 'Viewing docker image ' + image.id + ' under repository ' + namespace + '/' + name +
': Image changes tree and list view';
- }, function() {
- $rootScope.title = 'Unknown Image';
- $scope.loading = false;
- });
- // Fetch the image changes.
- var changesFetch = Restangular.one('repository/' + namespace + '/' + name + '/image/' + imageid + '/changes');
- changesFetch.get().then(function(changes) {
- var combinedChanges = [];
- var addCombinedChanges = function(c, kind) {
- for (var i = 0; i < c.length; ++i) {
- combinedChanges.push({
- 'kind': kind,
- 'file': c[i]
- });
- }
- };
+ // Fetch the image's changes.
+ fetchChanges();
- addCombinedChanges(changes.added, 'added');
- addCombinedChanges(changes.removed, 'removed');
- addCombinedChanges(changes.changed, 'changed');
+ $('#copyClipboard').clipboardCopy();
- $scope.combinedChanges = combinedChanges;
- $scope.imageChanges = changes;
- });
+ return image;
+ });
+ };
+
+ var fetchChanges = function() {
+ var changesFetch = Restangular.one('repository/' + namespace + '/' + name + '/image/' + imageid + '/changes');
+ changesFetch.get().then(function(changes) {
+ var combinedChanges = [];
+ var addCombinedChanges = function(c, kind) {
+ for (var i = 0; i < c.length; ++i) {
+ combinedChanges.push({
+ 'kind': kind,
+ 'file': c[i]
+ });
+ }
+ };
+
+ addCombinedChanges(changes.added, 'added');
+ addCombinedChanges(changes.removed, 'removed');
+ addCombinedChanges(changes.changed, 'changed');
+
+ $scope.combinedChanges = combinedChanges;
+ $scope.imageChanges = changes;
+ });
+ };
+
+ // Fetch the image.
+ fetchImage();
}
function V1Ctrl($scope, $location, UserService) {
- $scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
- $scope.user = currentUser;
- }, true);
+ UserService.updateUserIn($scope);
}
function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangular, PlanService) {
- $scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
- $scope.user = currentUser;
- }, true);
+ UserService.updateUserIn($scope);
$scope.repo = {
'is_public': 1,
@@ -835,6 +801,94 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula
});
});
+ // Watch the namespace on the repo. If it changes, we update the plan and the public/private
+ // accordingly.
+ $scope.isUserNamespace = true;
+ $scope.$watch('repo.namespace', function(namespace) {
+ // Note: Can initially be undefined.
+ if (!namespace) { return; }
+
+ var isUserNamespace = (namespace == $scope.user.username);
+
+ $scope.planRequired = null;
+ $scope.isUserNamespace = isUserNamespace;
+
+ if (isUserNamespace) {
+ // Load the user's subscription information in case they want to create a private
+ // repository.
+ PlanService.getSubscription(null, subscribedToPlan, function() {
+ PlanService.getMinimumPlan(1, false, function(minimum) { $scope.planRequired = minimum; });
+ });
+ } else {
+ $scope.planRequired = null;
+
+ var checkPrivateAllowed = Restangular.one('organization/' + namespace + '/private');
+ checkPrivateAllowed.get().then(function(resp) {
+ $scope.planRequired = resp.privateAllowed ? null : {};
+ }, function() {
+ $scope.planRequired = {};
+ });
+
+ // Auto-set to private repo.
+ $scope.repo.is_public = '0';
+ }
+ });
+
+ $scope.createNewRepo = function() {
+ $('#repoName').popover('hide');
+
+ var uploader = $('#file-drop')[0];
+ if ($scope.repo.initialize && uploader.files.length < 1) {
+ $('#missingfileModal').modal();
+ return;
+ }
+
+ $scope.creating = true;
+ var repo = $scope.repo;
+ var data = {
+ 'namespace': repo.namespace,
+ 'repository': repo.name,
+ 'visibility': repo.is_public == '1' ? 'public' : 'private',
+ 'description': repo.description
+ };
+
+ var createPost = Restangular.one('repository');
+ createPost.customPOST(data).then(function(created) {
+ $scope.creating = false;
+ $scope.created = created;
+
+ // Repository created. Start the upload process if applicable.
+ if ($scope.repo.initialize) {
+ startFileUpload(created);
+ return;
+ }
+
+ // Otherwise, redirect to the repo page.
+ $location.path('/repository/' + created.namespace + '/' + created.name);
+ }, function(result) {
+ $scope.creating = false;
+ $scope.createError = result.data;
+ $timeout(function() {
+ $('#repoName').popover('show');
+ });
+ });
+ };
+
+ $scope.upgradePlan = function() {
+ var callbacks = {
+ 'started': function() { $scope.planChanging = true; },
+ 'opened': function() { $scope.planChanging = true; },
+ 'closed': function() { $scope.planChanging = false; },
+ 'success': subscribedToPlan,
+ 'failure': function(resp) {
+ $('#couldnotsubscribeModal').modal();
+ $scope.planChanging = false;
+ }
+ };
+
+ PlanService.changePlan($scope, null, $scope.planRequired.stripeId, callbacks);
+ };
+
var startBuild = function(repo, fileId) {
$scope.building = true;
@@ -920,120 +974,19 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula
}
});
};
-
- $scope.createNewRepo = function() {
- $('#repoName').popover('hide');
-
- var uploader = $('#file-drop')[0];
- if ($scope.repo.initialize && uploader.files.length < 1) {
- $('#missingfileModal').modal();
- return;
- }
-
- $scope.creating = true;
- var repo = $scope.repo;
- var data = {
- 'namespace': repo.namespace,
- 'repository': repo.name,
- 'visibility': repo.is_public == '1' ? 'public' : 'private',
- 'description': repo.description
- };
-
- var createPost = Restangular.one('repository');
- createPost.customPOST(data).then(function(created) {
- $scope.creating = false;
- $scope.created = created;
-
- // Repository created. Start the upload process if applicable.
- if ($scope.repo.initialize) {
- startFileUpload(created);
- return;
- }
-
- // Otherwise, redirect to the repo page.
- $location.path('/repository/' + created.namespace + '/' + created.name);
- }, function(result) {
- $scope.creating = false;
- $scope.createError = result.data;
- $timeout(function() {
- $('#repoName').popover('show');
- });
- });
- };
-
- $scope.upgradePlan = function() {
- var callbacks = {
- 'started': function() { $scope.planChanging = true; },
- 'opened': function() { $scope.planChanging = true; },
- 'closed': function() { $scope.planChanging = false; },
- 'success': subscribedToPlan,
- 'failure': function(resp) {
- $('#couldnotsubscribeModal').modal();
- $scope.planChanging = false;
- }
- };
-
- PlanService.changePlan($scope, null, $scope.planRequired.stripeId, callbacks);
- };
-
- // Watch the namespace on the repo. If it changes, we update the plan and the public/private
- // accordingly.
- $scope.isUserNamespace = true;
- $scope.$watch('repo.namespace', function(namespace) {
- // Note: Can initially be undefined.
- if (!namespace) { return; }
-
- var isUserNamespace = (namespace == $scope.user.username);
-
- $scope.planRequired = null;
- $scope.isUserNamespace = isUserNamespace;
-
- if (isUserNamespace) {
- // Load the user's subscription information in case they want to create a private
- // repository.
- PlanService.getSubscription(null, subscribedToPlan, function() {
- PlanService.getMinimumPlan(1, false, function(minimum) { $scope.planRequired = minimum; });
- });
- } else {
- $scope.planRequired = null;
-
- var checkPrivateAllowed = Restangular.one('organization/' + namespace + '/private');
- checkPrivateAllowed.get().then(function(resp) {
- $scope.planRequired = resp.privateAllowed ? null : {};
- }, function() {
- $scope.planRequired = {};
- });
-
- // Auto-set to private repo.
- $scope.repo.is_public = '0';
- }
- });
}
-function OrgViewCtrl($rootScope, $scope, Restangular, $routeParams) {
+function OrgViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams) {
+ var orgname = $routeParams.orgname;
+
$('.info-icon').popover({
- 'trigger': 'hover',
- 'html': true
+ 'trigger': 'hover',
+ 'html': true
});
$scope.TEAM_PATTERN = TEAM_PATTERN;
$rootScope.title = 'Loading...';
- var orgname = $routeParams.orgname;
-
- var loadOrganization = function() {
- var getOrganization = Restangular.one(getRestUrl('organization', orgname));
- getOrganization.get().then(function(resp) {
- $scope.organization = resp;
- $scope.loading = false;
-
- $rootScope.title = orgname;
- $rootScope.description = 'Viewing organization ' + orgname;
- }, function() {
- $scope.loading = false;
- });
- };
-
$scope.teamRoles = [
{ 'id': 'member', 'title': 'Member', 'kind': 'default' },
{ 'id': 'creator', 'title': 'Creator', 'kind': 'success' },
@@ -1093,10 +1046,21 @@ function OrgViewCtrl($rootScope, $scope, Restangular, $routeParams) {
});
};
+ var loadOrganization = function() {
+ $scope.orgResource = ApiService.at('organization', orgname).get(function(org) {
+ $scope.organization = org;
+ $rootScope.title = orgname;
+ $rootScope.description = 'Viewing organization ' + orgname;
+ });
+ };
+
+ // Load the organization.
loadOrganization();
}
-function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService, PlanService) {
+function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService, PlanService, ApiService) {
+ var orgname = $routeParams.orgname;
+
// Load the list of plans.
PlanService.getPlans(function(plans) {
$scope.plans = plans.business;
@@ -1112,8 +1076,6 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService
addPlans(plans.business);
});
- var orgname = $routeParams.orgname;
-
$scope.orgname = orgname;
$scope.membersLoading = true;
$scope.membersFound = null;
@@ -1163,35 +1125,32 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService
};
var loadOrganization = function() {
- var getOrganization = Restangular.one(getRestUrl('organization', orgname));
- getOrganization.get().then(function(resp) {
- if (resp && resp.is_admin) {
- $scope.organization = resp;
+ $scope.orgResource = ApiService.at('organization', orgname).get(function(org) {
+ if (org && org.is_admin) {
+ $scope.organization = org;
$rootScope.title = orgname + ' (Admin)';
$rootScope.description = 'Administration page for organization ' + orgname;
}
-
- $scope.loading = false;
- }, function() {
- $scope.loading = false;
});
};
+ // Load the organization.
loadOrganization();
}
-function TeamViewCtrl($rootScope, $scope, Restangular, $routeParams) {
+function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams) {
$('.info-icon').popover({
- 'trigger': 'hover',
- 'html': true
+ 'trigger': 'hover',
+ 'html': true
});
- $scope.orgname = $routeParams.orgname;
var teamname = $routeParams.teamname;
+ var orgname = $routeParams.orgname;
+
+ $scope.orgname = orgname;
+ $scope.teamname = teamname;
$rootScope.title = 'Loading...';
- $scope.loading = true;
- $scope.teamname = teamname;
$scope.addNewMember = function(member) {
if ($scope.members[member.name]) { return; }
@@ -1225,57 +1184,38 @@ function TeamViewCtrl($rootScope, $scope, Restangular, $routeParams) {
};
var loadOrganization = function() {
- var getOrganization = Restangular.one(getRestUrl('organization', $scope.orgname))
- getOrganization.get().then(function(resp) {
- $scope.organization = resp;
+ $scope.orgResource = ApiService.at('organization', orgname).get(function(org) {
+ $scope.organization = org;
$scope.team = $scope.organization.teams[teamname];
- $scope.loading = !$scope.organization || !$scope.members;
- }, function() {
- $scope.organization = null;
- $scope.members = null;
- $scope.loading = false;
+ $rootScope.title = teamname + ' (' + $scope.orgname + ')';
+ $rootScope.description = 'Team management page for team ' + teamname + ' under organization ' + $scope.orgname;
+ loadMembers();
+ return org;
});
};
var loadMembers = function() {
- var getMembers = Restangular.one(getRestUrl('organization', $scope.orgname, 'team', teamname, 'members'));
- getMembers.get().then(function(resp) {
+ $scope.membersResource = ApiService.at('organization', $scope.orgname, 'team', teamname, 'members').get(function(resp) {
$scope.members = resp.members;
$scope.canEditMembers = resp.can_edit;
- $scope.loading = !$scope.organization || !$scope.members;
- $rootScope.title = teamname + ' (' + $scope.orgname + ')';
- $rootScope.description = 'Team management page for team ' + teamname + ' under organization ' + $scope.orgname;
- }, function() {
- $scope.organization = null;
- $scope.members = null;
- $scope.loading = false;
- });
+ return resp.members;
+ });
};
+ // Load the organization.
loadOrganization();
- loadMembers();
}
function OrgsCtrl($scope, UserService) {
- $scope.loading = true;
-
- $scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
- $scope.user = currentUser;
- $scope.loading = false;
- }, true);
+ UserService.updateUserIn($scope);
browserchrome.update();
}
function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, PlanService, Restangular) {
- $scope.loading = true;
-
- $scope.$watch(function () { return UserService.currentUser(); }, function (currentUser) {
- $scope.user = currentUser;
- $scope.loading = false;
- }, true);
+ UserService.updateUserIn($scope);
- requested = $routeParams['plan'];
+ var requested = $routeParams['plan'];
// Load the list of plans.
PlanService.getPlans(function(plans) {
@@ -1314,13 +1254,13 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan
var createPost = Restangular.one('organization/');
createPost.customPOST(data).then(function(created) {
- $scope.creating = false;
$scope.created = created;
// Reset the organizations list.
UserService.load();
var showOrg = function() {
+ $scope.creating = false;
$location.path('/organization/' + org.name + '/');
};
@@ -1331,6 +1271,7 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan
}
// Otherwise, show the subscribe for the plan.
+ $scope.creating = true;
var callbacks = {
'opened': function() { $scope.creating = true; },
'closed': showOrg,
@@ -1338,7 +1279,7 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan
'failure': showOrg
};
- PlanService.changePlan($scope, org.name, $scope.currentPlan.stripeId, false, callbacks);
+ PlanService.changePlan($scope, org.name, $scope.currentPlan.stripeId, callbacks);
}, function(result) {
$scope.creating = false;
$scope.createError = result.data.message || result.data;
@@ -1350,49 +1291,38 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan
}
-function OrgMemberLogsCtrl($scope, $routeParams, $rootScope, $timeout, Restangular) {
+function OrgMemberLogsCtrl($scope, $routeParams, $rootScope, $timeout, Restangular, ApiService) {
var orgname = $routeParams.orgname;
var membername = $routeParams.membername;
$scope.orgname = orgname;
- $scope.loading = true;
$scope.memberInfo = null;
$scope.ready = false;
- var checkReady = function() {
- $scope.loading = !$scope.organization || !$scope.memberInfo;
- if (!$scope.loading) {
- $rootScope.title = 'Logs for ' + $scope.memberInfo.username + ' (' + $scope.orgname + ')';
- $rootScope.description = 'Shows all the actions of ' + $scope.memberInfo.username +
- ' under organization ' + $scope.orgname;
- $timeout(function() {
- $scope.ready = true;
- });
- }
- };
-
var loadOrganization = function() {
- var getOrganization = Restangular.one(getRestUrl('organization', orgname))
- getOrganization.get().then(function(resp) {
- $scope.organization = resp;
- checkReady();
- }, function() {
- $scope.organization = null;
- $scope.loading = false;
+ $scope.orgResource = ApiService.at('organization', orgname).get(function(org) {
+ $scope.organization = org;
+ return org;
});
};
var loadMemberInfo = function() {
- var getMemberInfo = Restangular.one(getRestUrl('organization', orgname, 'members', membername))
- getMemberInfo.get().then(function(resp) {
+ $scope.memberResource = ApiService.at('organization', $scope.orgname, 'members', membername).get(function(resp) {
$scope.memberInfo = resp.member;
- checkReady();
- }, function() {
- $scope.memberInfo = null;
- $scope.loading = false;
- });
+
+ $rootScope.title = 'Logs for ' + $scope.memberInfo.username + ' (' + $scope.orgname + ')';
+ $rootScope.description = 'Shows all the actions of ' + $scope.memberInfo.username +
+ ' under organization ' + $scope.orgname;
+
+ $timeout(function() {
+ $scope.ready = true;
+ });
+
+ return resp.member;
+ });
};
+ // Load the org info and the member info.
loadOrganization();
loadMemberInfo();
}
\ No newline at end of file
diff --git a/static/js/graphing.js b/static/js/graphing.js
index d92a6bfa3..fbf5f2162 100644
--- a/static/js/graphing.js
+++ b/static/js/graphing.js
@@ -54,7 +54,7 @@ ImageHistoryTree.prototype.calculateDimensions_ = function(container) {
var cw = Math.max(document.getElementById(container).clientWidth, this.maxWidth_ * DEPTH_WIDTH);
var ch = this.maxHeight_ * (DEPTH_HEIGHT + 10);
- var margin = { top: 40, right: 20, bottom: 20, left: 40 };
+ var margin = { top: 40, right: 20, bottom: 20, left: 80 };
var m = [margin.top, margin.right, margin.bottom, margin.left];
var w = cw - m[1] - m[3];
var h = ch - m[0] - m[2];
@@ -87,6 +87,7 @@ ImageHistoryTree.prototype.updateDimensions_ = function() {
var viewportHeight = $(window).height();
var boundingBox = document.getElementById(container).getBoundingClientRect();
document.getElementById(container).style.maxHeight = (viewportHeight - boundingBox.top - 110) + 'px';
+
$('#' + container).overscroll();
// Update the tree.
@@ -94,9 +95,13 @@ ImageHistoryTree.prototype.updateDimensions_ = function() {
var tree = this.tree_;
var vis = this.vis_;
+
+ var ow = w + m[1] + m[3];
+ var oh = h + m[0] + m[2];
rootSvg
- .attr("width", w + m[1] + m[3])
- .attr("height", h + m[0] + m[2]);
+ .attr("width", ow)
+ .attr("height", oh)
+ .attr("style", "width: " + ow + "px; height: " + oh + "px");
tree.size([w, h]);
vis.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
diff --git a/static/lib/jquery.overscroll.js b/static/lib/jquery.overscroll.js
new file mode 100644
index 000000000..7126a8b1e
--- /dev/null
+++ b/static/lib/jquery.overscroll.js
@@ -0,0 +1,793 @@
+/**
+ * Overscroll v1.7.3
+ * A jQuery Plugin that emulates the iPhone scrolling experience in a browser.
+ * http://azoffdesign.com/overscroll
+ *
+ * Intended for use with the latest jQuery
+ * http://code.jquery.com/jquery-latest.js
+ *
+ * Copyright 2013, Jonathan Azoff
+ * Licensed under the MIT license.
+ * https://github.com/azoff/overscroll/blob/master/mit.license
+ *
+ * For API documentation, see the README file
+ * http://azof.fr/pYCzuM
+ *
+ * Date: Tuesday, March 18th 2013
+ */
+(function(global, dom, browser, math, wait, cancel, namespace, $, none){
+
+ // We want to run this plug-in in strict-mode
+ // so that we may benefit from its optimizations
+ 'use strict';
+
+ // The key used to bind-instance specific data to an object
+ var datakey = 'overscroll';
+
+ // create node if there's not one present (e.g., for test runners)
+ if (dom.body === null) {
+ dom.documentElement.appendChild(
+ dom.createElement('body')
+ );
+ }
+
+ // quick fix for IE 8 and below since getComputedStyle() is not supported
+ // TODO: find a better solution
+ if (!global.getComputedStyle) {
+ global.getComputedStyle = function (el, pseudo) {
+ this.el = el;
+ this.getPropertyValue = function (prop) {
+ var re = /(\-([a-z]){1})/g;
+ if (prop == 'float') prop = 'styleFloat';
+ if (re.test(prop)) {
+ prop = prop.replace(re, function () {
+ return arguments[2].toUpperCase();
+ });
+ }
+ return el.currentStyle[prop] ? el.currentStyle[prop] : null;
+ };
+ return this;
+ };
+ }
+
+ // runs feature detection for overscroll
+ var compat = {
+ animate: (function(){
+ var fn = global.requestAnimationFrame ||
+ global.webkitRequestAnimationFrame ||
+ global.mozRequestAnimationFrame ||
+ global.oRequestAnimationFrame ||
+ global.msRequestAnimationFrame ||
+ function(callback) { wait(callback, 1000/60); };
+ return function(callback) {
+ fn.call(global, callback);
+ };
+ })(),
+ overflowScrolling: (function(){
+ var style = '';
+ var div = dom.createElement('div');
+ var prefixes = ['webkit', 'moz', 'o', 'ms'];
+ dom.body.appendChild(div);
+ $.each(prefixes, function(i, prefix){
+ div.style[prefix + 'OverflowScrolling'] = 'touch';
+ });
+ div.style.overflowScrolling = 'touch';
+ var computedStyle = global.getComputedStyle(div);
+ if (!!computedStyle.overflowScrolling) {
+ style = 'overflow-scrolling';
+ } else {
+ $.each(prefixes, function(i, prefix){
+ if (!!computedStyle[prefix + 'OverflowScrolling']) {
+ style = '-' + prefix + '-overflow-scrolling';
+ }
+ return !style;
+ });
+ }
+ div.parentNode.removeChild(div);
+ return style;
+ })(),
+ cursor: (function() {
+ var div = dom.createElement('div');
+ var prefixes = ['webkit', 'moz'];
+ var gmail = 'https://mail.google.com/mail/images/2/';
+ var style = {
+ grab: 'url('+gmail+'openhand.cur), move',
+ grabbing: 'url('+gmail+'closedhand.cur), move'
+ };
+ dom.body.appendChild(div);
+ $.each(prefixes, function(i, prefix){
+ var found, cursor = '-' + prefix + '-grab';
+ div.style.cursor = cursor;
+ var computedStyle = global.getComputedStyle(div);
+ found = computedStyle.cursor === cursor;
+ if (found) {
+ style = {
+ grab: '-' + prefix + '-grab',
+ grabbing: '-' + prefix + '-grabbing'
+ };
+ }
+ return !found;
+ });
+ div.parentNode.removeChild(div);
+ return style;
+ })()
+ };
+
+ // These are all the events that could possibly
+ // be used by the plug-in
+ var events = {
+ drag: 'mousemove touchmove',
+ end: 'mouseup mouseleave click touchend touchcancel',
+ hover: 'mouseenter mouseleave',
+ ignored: 'select dragstart drag',
+ scroll: 'scroll',
+ start: 'mousedown touchstart',
+ wheel: 'mousewheel DOMMouseScroll'
+ };
+
+ // These settings are used to tweak drift settings
+ // for the plug-in
+ var settings = {
+ captureThreshold: 3,
+ driftDecay: 1.1,
+ driftSequences: 22,
+ driftTimeout: 100,
+ scrollDelta: 15,
+ thumbOpacity: 0.7,
+ thumbThickness: 6,
+ thumbTimeout: 400,
+ wheelDelta: 20,
+ wheelTicks: 120
+ };
+
+ // These defaults are used to complement any options
+ // passed into the plug-in entry point
+ var defaults = {
+ cancelOn: 'select,input,textarea',
+ direction: 'multi',
+ dragHold: false,
+ hoverThumbs: false,
+ scrollDelta: settings.scrollDelta,
+ showThumbs: true,
+ persistThumbs: false,
+ captureWheel: true,
+ wheelDelta: settings.wheelDelta,
+ wheelDirection: 'multi',
+ zIndex: 999,
+ ignoreSizing: false
+ };
+
+ // Triggers a DOM event on the overscrolled element.
+ // All events are namespaced under the overscroll name
+ function triggerEvent(event, target) {
+ target.trigger('overscroll:' + event);
+ }
+
+ // Utility function to return a timestamp
+ function time() {
+ return (new Date()).getTime();
+ }
+
+ // Captures the position from an event, modifies the properties
+ // of the second argument to persist the position, and then
+ // returns the modified object
+ function capturePosition(event, position, index) {
+ position.x = event.pageX;
+ position.y = event.pageY;
+ position.time = time();
+ position.index = index;
+ return position;
+ }
+
+ // Used to move the thumbs around an overscrolled element
+ function moveThumbs(thumbs, sizing, left, top) {
+
+ var ml, mt;
+
+ if (thumbs && thumbs.added) {
+ if (thumbs.horizontal) {
+ ml = left * (1 + sizing.container.width / sizing.container.scrollWidth);
+ mt = top + sizing.thumbs.horizontal.top;
+ thumbs.horizontal.css('margin', mt + 'px 0 0 ' + ml + 'px');
+ }
+ if (thumbs.vertical) {
+ ml = left + sizing.thumbs.vertical.left;
+ mt = top * (1 + sizing.container.height / sizing.container.scrollHeight);
+ thumbs.vertical.css('margin', mt + 'px 0 0 ' + ml + 'px');
+ }
+ }
+
+ }
+
+ // Used to toggle the thumbs on and off
+ // of an overscrolled element
+ function toggleThumbs(thumbs, options, dragging) {
+ if (thumbs && thumbs.added && !options.persistThumbs) {
+ if (dragging) {
+ if (thumbs.vertical) {
+ thumbs.vertical.stop(true, true).fadeTo('fast', settings.thumbOpacity);
+ }
+ if (thumbs.horizontal) {
+ thumbs.horizontal.stop(true, true).fadeTo('fast', settings.thumbOpacity);
+ }
+ } else {
+ if (thumbs.vertical) {
+ thumbs.vertical.fadeTo('fast', 0);
+ }
+ if (thumbs.horizontal) {
+ thumbs.horizontal.fadeTo('fast', 0);
+ }
+ }
+ }
+ }
+
+ // Defers click event listeners to after a mouseup event.
+ // Used to avoid unintentional clicks
+ function deferClick(target) {
+ var clicks, key = 'events';
+ var events = $._data ? $._data(target[0], key) : target.data(key);
+ if (events && events.click) {
+ clicks = events.click.slice();
+ target.off('click').one('click', function(){
+ $.each(clicks, function(i, click){
+ target.click(click);
+ }); return false;
+ });
+ }
+ }
+
+ // Toggles thumbs on hover. This event is only triggered
+ // if the hoverThumbs option is set
+ function hover(event) {
+ var data = event.data,
+ thumbs = data.thumbs,
+ options = data.options,
+ dragging = event.type === 'mouseenter';
+ toggleThumbs(thumbs, options, dragging);
+ }
+
+ // This function is only ever used when the overscrolled element
+ // scrolled outside of the scope of this plugin.
+ function scroll(event) {
+ var data = event.data;
+ if (!data.flags.dragged) {
+ /*jshint validthis:true */
+ moveThumbs(data.thumbs, data.sizing, this.scrollLeft, this.scrollTop);
+ }
+ }
+
+ // handles mouse wheel scroll events
+ function wheel(event) {
+
+ // prevent any default wheel behavior
+ event.preventDefault();
+
+ var data = event.data,
+ options = data.options,
+ sizing = data.sizing,
+ thumbs = data.thumbs,
+ dwheel = data.wheel,
+ flags = data.flags,
+ original = event.originalEvent,
+ delta = 0, deltaX = 0, deltaY = 0;
+
+ // stop any drifts
+ flags.drifting = false;
+
+ // normalize the wheel ticks
+ if (original.detail) {
+ delta = -original.detail;
+ if (original.detailX) {
+ deltaX = -original.detailX;
+ }
+ if (original.detailY) {
+ deltaY = -original.detailY;
+ }
+ } else if (original.wheelDelta) {
+ delta = original.wheelDelta / settings.wheelTicks;
+ if (original.wheelDeltaX) {
+ deltaX = original.wheelDeltaX / settings.wheelTicks;
+ }
+ if (original.wheelDeltaY) {
+ deltaY = original.wheelDeltaY / settings.wheelTicks;
+ }
+ }
+
+ // apply a pixel delta to each tick
+ delta *= options.wheelDelta;
+ deltaX *= options.wheelDelta;
+ deltaY *= options.wheelDelta;
+
+ // initialize flags if this is the first tick
+ if (!dwheel) {
+ data.target.data(datakey).dragging = flags.dragging = true;
+ data.wheel = dwheel = { timeout: null };
+ toggleThumbs(thumbs, options, true);
+ }
+
+ // actually modify scroll offsets
+ if (options.wheelDirection === 'vertical'){
+ /*jshint validthis:true */
+ this.scrollTop -= delta;
+ } else if ( options.wheelDirection === 'horizontal') {
+ this.scrollLeft -= delta;
+ } else {
+ this.scrollLeft -= deltaX;
+ this.scrollTop -= deltaY || delta;
+ }
+
+ if (dwheel.timeout) { cancel(dwheel.timeout); }
+
+ moveThumbs(thumbs, sizing, this.scrollLeft, this.scrollTop);
+
+ dwheel.timeout = wait(function() {
+ data.target.data(datakey).dragging = flags.dragging = false;
+ toggleThumbs(thumbs, options, data.wheel = null);
+ }, settings.thumbTimeout);
+
+ }
+
+ // updates the current scroll offset during a mouse move
+ function drag(event) {
+
+ event.preventDefault();
+
+ var data = event.data,
+ touches = event.originalEvent.touches,
+ options = data.options,
+ sizing = data.sizing,
+ thumbs = data.thumbs,
+ position = data.position,
+ flags = data.flags,
+ target = data.target.get(0);
+
+
+ // correct page coordinates for touch devices
+ if (touches && touches.length) {
+ event = touches[0];
+ }
+
+ if (!flags.dragged) {
+ toggleThumbs(thumbs, options, true);
+ }
+
+ flags.dragged = true;
+
+ if (options.direction !== 'vertical') {
+ target.scrollLeft -= (event.pageX - position.x);
+ }
+
+ if (data.options.direction !== 'horizontal') {
+ target.scrollTop -= (event.pageY - position.y);
+ }
+
+ capturePosition(event, data.position);
+
+ if (--data.capture.index <= 0) {
+ data.target.data(datakey).dragging = flags.dragging = true;
+ capturePosition(event, data.capture, settings.captureThreshold);
+ }
+
+ moveThumbs(thumbs, sizing, target.scrollLeft, target.scrollTop);
+
+ }
+
+ // sends the overscrolled element into a drift
+ function drift(target, event, callback) {
+
+ var data = event.data, dx, dy, xMod, yMod,
+ capture = data.capture,
+ options = data.options,
+ sizing = data.sizing,
+ thumbs = data.thumbs,
+ elapsed = time() - capture.time,
+ scrollLeft = target.scrollLeft,
+ scrollTop = target.scrollTop,
+ decay = settings.driftDecay;
+
+ // only drift if enough time has passed since
+ // the last capture event
+ if (elapsed > settings.driftTimeout) {
+ callback(data); return;
+ }
+
+ // determine offset between last capture and current time
+ dx = options.scrollDelta * (event.pageX - capture.x);
+ dy = options.scrollDelta * (event.pageY - capture.y);
+
+ // update target scroll offsets
+ if (options.direction !== 'vertical') {
+ scrollLeft -= dx;
+ } if (options.direction !== 'horizontal') {
+ scrollTop -= dy;
+ }
+
+ // split the distance to travel into a set of sequences
+ xMod = dx / settings.driftSequences;
+ yMod = dy / settings.driftSequences;
+
+ triggerEvent('driftstart', data.target);
+
+ data.drifting = true;
+
+ // animate the drift sequence
+ compat.animate(function render() {
+ if (data.drifting) {
+ var min = 1, max = -1;
+ data.drifting = false;
+ if (yMod > min && target.scrollTop > scrollTop || yMod < max && target.scrollTop < scrollTop) {
+ data.drifting = true;
+ target.scrollTop -= yMod;
+ yMod /= decay;
+ }
+ if (xMod > min && target.scrollLeft > scrollLeft || xMod < max && target.scrollLeft < scrollLeft) {
+ data.drifting = true;
+ target.scrollLeft -= xMod;
+ xMod /= decay;
+ }
+ moveThumbs(thumbs, sizing, target.scrollLeft, target.scrollTop);
+ compat.animate(render);
+ } else {
+ triggerEvent('driftend', data.target);
+ callback(data);
+ }
+ });
+
+ }
+
+ // starts the drag operation and binds the mouse move handler
+ function start(event) {
+
+ var data = event.data,
+ touches = event.originalEvent.touches,
+ target = data.target,
+ dstart = data.start = $(event.target),
+ flags = data.flags;
+
+ // stop any drifts
+ flags.drifting = false;
+
+ // only start drag if the user has not explictly banned it.
+ if (dstart.size() && !dstart.is(data.options.cancelOn)) {
+
+ // without this the simple "click" event won't be recognized on touch clients
+ if (!touches) { event.preventDefault(); }
+
+ if (!compat.overflowScrolling) {
+ target.css('cursor', compat.cursor.grabbing);
+ target.data(datakey).dragging = flags.dragging = flags.dragged = false;
+
+ // apply the drag listeners to the doc or target
+ if(data.options.dragHold) {
+ $(document).on(events.drag, data, drag);
+ } else {
+ target.on(events.drag, data, drag);
+ }
+ }
+
+ data.position = capturePosition(event, {});
+ data.capture = capturePosition(event, {}, settings.captureThreshold);
+ triggerEvent('dragstart', target);
+
+ }
+
+ }
+
+ // ends the drag operation and unbinds the mouse move handler
+ function stop(event) {
+
+ var data = event.data,
+ target = data.target,
+ options = data.options,
+ flags = data.flags,
+ thumbs = data.thumbs,
+
+ // hides the thumbs after the animation is done
+ done = function () {
+ if (thumbs && !options.hoverThumbs) {
+ toggleThumbs(thumbs, options, false);
+ }
+ };
+
+ // remove drag listeners from doc or target
+ if(options.dragHold) {
+ $(document).unbind(events.drag, drag);
+ } else {
+ target.unbind(events.drag, drag);
+ }
+
+ // only fire events and drift if we started with a
+ // valid position
+ if (data.position) {
+
+ triggerEvent('dragend', target);
+
+ // only drift if a drag passed our threshold
+ if (flags.dragging && !compat.overflowScrolling) {
+ drift(target.get(0), event, done);
+ } else {
+ done();
+ }
+
+ }
+
+ // only if we moved, and the mouse down is the same as
+ // the mouse up target do we defer the event
+ if (flags.dragging && !compat.overflowScrolling && data.start && data.start.is(event.target)) {
+ deferClick(data.start);
+ }
+
+ // clear all internal flags and settings
+ target.data(datakey).dragging =
+ data.start =
+ data.capture =
+ data.position =
+ flags.dragged =
+ flags.dragging = false;
+
+ // set the cursor back to normal
+ target.css('cursor', compat.cursor.grab);
+
+ }
+
+ // Ensures that a full set of options are provided
+ // for the plug-in. Also does some validation
+ function getOptions(options) {
+
+ // fill in missing values with defaults
+ options = $.extend({}, defaults, options);
+
+ // check for inconsistent directional restrictions
+ if (options.direction !== 'multi' && options.direction !== options.wheelDirection) {
+ options.wheelDirection = options.direction;
+ }
+
+ // ensure positive values for deltas
+ options.scrollDelta = math.abs(parseFloat(options.scrollDelta));
+ options.wheelDelta = math.abs(parseFloat(options.wheelDelta));
+
+ // fix values for scroll offset
+ options.scrollLeft = options.scrollLeft === none ? null : math.abs(parseFloat(options.scrollLeft));
+ options.scrollTop = options.scrollTop === none ? null : math.abs(parseFloat(options.scrollTop));
+
+ return options;
+
+ }
+
+ // Returns the sizing information (bounding box) for the
+ // target DOM element
+ function getSizing(target) {
+
+ var $target = $(target),
+ width = $target.width(),
+ height = $target.height(),
+ scrollWidth = width >= target.scrollWidth ? width : target.scrollWidth,
+ scrollHeight = height >= target.scrollHeight ? height : target.scrollHeight,
+ hasScroll = scrollWidth > width || scrollHeight > height;
+
+ return {
+ valid: hasScroll,
+ container: {
+ width: width,
+ height: height,
+ scrollWidth: scrollWidth,
+ scrollHeight: scrollHeight
+ },
+ thumbs: {
+ horizontal: {
+ width: width * width / scrollWidth,
+ height: settings.thumbThickness,
+ corner: settings.thumbThickness / 2,
+ left: 0,
+ top: height - settings.thumbThickness
+ },
+ vertical: {
+ width: settings.thumbThickness,
+ height: height * height / scrollHeight,
+ corner: settings.thumbThickness / 2,
+ left: width - settings.thumbThickness,
+ top: 0
+ }
+ }
+ };
+
+ }
+
+ // Attempts to get (or implicitly creates) the
+ // remover function for the target passed
+ // in as an argument
+ function getRemover(target, orCreate) {
+
+ var $target = $(target), thumbs,
+ data = $target.data(datakey) || {},
+ style = $target.attr('style'),
+ fallback = orCreate ? function () {
+
+ data = $target.data(datakey);
+ thumbs = data.thumbs;
+
+ // restore original styles (if any)
+ if (style) {
+ $target.attr('style', style);
+ } else {
+ $target.removeAttr('style');
+ }
+
+ // remove any created thumbs
+ if (thumbs) {
+ if (thumbs.horizontal) { thumbs.horizontal.remove(); }
+ if (thumbs.vertical) { thumbs.vertical.remove(); }
+ }
+
+ // remove any bound overscroll events and data
+ $target
+ .removeData(datakey)
+ .off(events.wheel, wheel)
+ .off(events.start, start)
+ .off(events.end, stop)
+ .off(events.ignored, ignore);
+
+ } : $.noop;
+
+ return $.isFunction(data.remover) ? data.remover : fallback;
+
+ }
+
+ // Genterates CSS specific to a particular thumb.
+ // It requires sizing data and options
+ function getThumbCss(size, options) {
+ return {
+ position: 'absolute',
+ opacity: options.persistThumbs ? settings.thumbOpacity : 0,
+ 'background-color': 'black',
+ width: size.width + 'px',
+ height: size.height + 'px',
+ 'border-radius': size.corner + 'px',
+ 'margin': size.top + 'px 0 0 ' + size.left + 'px',
+ 'z-index': options.zIndex
+ };
+ }
+
+ // Creates the DOM elements used as "thumbs" within
+ // the target container.
+ function createThumbs(target, sizing, options) {
+
+ var div = '
',
+ thumbs = {},
+ css = false;
+
+ if (sizing.container.scrollWidth > 0 && options.direction !== 'vertical') {
+ css = getThumbCss(sizing.thumbs.horizontal, options);
+ thumbs.horizontal = $(div).css(css).prependTo(target);
+ }
+
+ if (sizing.container.scrollHeight > 0 && options.direction !== 'horizontal') {
+ css = getThumbCss(sizing.thumbs.vertical, options);
+ thumbs.vertical = $(div).css(css).prependTo(target);
+ }
+
+ thumbs.added = !!css;
+
+ return thumbs;
+
+ }
+
+ // ignores events on the overscroll element
+ function ignore(event) {
+ event.preventDefault();
+ }
+
+ // This function takes a jQuery element, some
+ // (optional) options, and sets up event metadata
+ // for each instance the plug-in affects
+ function setup(target, options) {
+
+ // create initial data properties for this instance
+ options = getOptions(options);
+ var sizing = getSizing(target),
+ thumbs, data = {
+ options: options, sizing: sizing,
+ flags: { dragging: false },
+ remover: getRemover(target, true)
+ };
+
+ // only apply handlers if the overscrolled element
+ // actually has an area to scroll
+ if (sizing.valid || options.ignoreSizing) {
+ // provide a circular-reference, enable events, and
+ // apply any required CSS
+ data.target = target = $(target).css({
+ position: 'relative',
+ cursor: compat.cursor.grab
+ }).on(events.start, data, start)
+ .on(events.end, data, stop)
+ .on(events.ignored, data, ignore);
+
+ // apply the stop listeners for drag end
+ if(options.dragHold) {
+ $(document).on(events.end, data, stop);
+ } else {
+ data.target.on(events.end, data, stop);
+ }
+
+ // apply any user-provided scroll offsets
+ if (options.scrollLeft !== null) {
+ target.scrollLeft(options.scrollLeft);
+ } if (options.scrollTop !== null) {
+ target.scrollTop(options.scrollTop);
+ }
+
+ // use native oversroll, if it exists
+ if (compat.overflowScrolling) {
+ target.css(compat.overflowScrolling, 'touch');
+ } else {
+ target.on(events.scroll, data, scroll);
+ }
+
+ // check to see if the user would like mousewheel support
+ if (options.captureWheel) {
+ target.on(events.wheel, data, wheel);
+ }
+
+ // add thumbs and listeners (if we're showing them)
+ if (options.showThumbs) {
+ if (compat.overflowScrolling) {
+ target.css('overflow', 'scroll');
+ } else {
+ target.css('overflow', 'hidden');
+ data.thumbs = thumbs = createThumbs(target, sizing, options);
+ if (thumbs.added) {
+ moveThumbs(thumbs, sizing, target.scrollLeft(), target.scrollTop());
+ if (options.hoverThumbs) {
+ target.on(events.hover, data, hover);
+ }
+ }
+ }
+ } else {
+ target.css('overflow', 'hidden');
+ }
+
+ target.data(datakey, data);
+ }
+
+ }
+
+ // Removes any event listeners and other instance-specific
+ // data from the target. It attempts to leave the target
+ // at the state it found it.
+ function teardown(target) {
+ getRemover(target)();
+ }
+
+ // This is the entry-point for enabling the plug-in;
+ // You can find it's exposure point at the end
+ // of this closure
+ function overscroll(options) {
+ /*jshint validthis:true */
+ return this.removeOverscroll().each(function() {
+ setup(this, options);
+ });
+ }
+
+ // This is the entry-point for disabling the plug-in;
+ // You can find it's exposure point at the end
+ // of this closure
+ function removeOverscroll() {
+ /*jshint validthis:true */
+ return this.each(function () {
+ teardown(this);
+ });
+ }
+
+ // Extend overscroll to expose settings to the user
+ overscroll.settings = settings;
+
+ // Extend jQuery's prototype to expose the plug-in.
+ // If the supports native overflowScrolling, overscroll will not
+ // attempt to override the browser's built in support
+ $.extend(namespace, {
+ overscroll: overscroll,
+ removeOverscroll: removeOverscroll
+ });
+
+})(window, document, navigator, Math, setTimeout, clearTimeout, jQuery.fn, jQuery);
diff --git a/static/lib/loading-bar.css b/static/lib/loading-bar.css
new file mode 100755
index 000000000..7d2e88e16
--- /dev/null
+++ b/static/lib/loading-bar.css
@@ -0,0 +1,102 @@
+
+/* Make clicks pass-through */
+#loading-bar,
+#loading-bar-spinner {
+ pointer-events: none;
+ -webkit-pointer-events: none;
+ -webkit-transition: 0.5s linear all;
+ -moz-transition: 0.5s linear all;
+ -o-transition: 0.5s linear all;
+ transition: 0.5s linear all;
+}
+
+#loading-bar.ng-enter,
+#loading-bar.ng-leave.ng-leave-active,
+#loading-bar-spinner.ng-enter,
+#loading-bar-spinner.ng-leave.ng-leave-active {
+ opacity: 0;
+}
+
+#loading-bar.ng-enter.ng-enter-active,
+#loading-bar.ng-leave,
+#loading-bar-spinner.ng-enter.ng-enter-active,
+#loading-bar-spinner.ng-leave {
+ opacity: 1;
+}
+
+#loading-bar .bar {
+ -webkit-transition: width 350ms;
+ -moz-transition: width 350ms;
+ -o-transition: width 350ms;
+ transition: width 350ms;
+
+ background: #29d;
+ position: fixed;
+ z-index: 2000;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 2px;
+}
+
+/* Fancy blur effect */
+#loading-bar .peg {
+ display: block;
+ position: absolute;
+ right: 0px;
+ width: 100px;
+ height: 100%;
+ box-shadow: 0 0 10px #29d, 0 0 5px #29d;
+ opacity: 1.0;
+
+ -webkit-transform: rotate(3deg) translate(0px, -4px);
+ -moz-transform: rotate(3deg) translate(0px, -4px);
+ -ms-transform: rotate(3deg) translate(0px, -4px);
+ -o-transform: rotate(3deg) translate(0px, -4px);
+ transform: rotate(3deg) translate(0px, -4px);
+}
+
+#loading-bar-spinner {
+ display: block;
+ position: fixed;
+ z-index: 100;
+ top: 10px;
+ left: 10px;
+}
+
+#loading-bar-spinner .spinner-icon {
+ width: 14px;
+ height: 14px;
+
+ border: solid 2px transparent;
+ border-top-color: #29d;
+ border-left-color: #29d;
+ border-radius: 10px;
+
+ -webkit-animation: loading-bar-spinner 400ms linear infinite;
+ -moz-animation: loading-bar-spinner 400ms linear infinite;
+ -ms-animation: loading-bar-spinner 400ms linear infinite;
+ -o-animation: loading-bar-spinner 400ms linear infinite;
+ animation: loading-bar-spinner 400ms linear infinite;
+}
+
+@-webkit-keyframes loading-bar-spinner {
+ 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); }
+ 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); }
+}
+@-moz-keyframes loading-bar-spinner {
+ 0% { -moz-transform: rotate(0deg); transform: rotate(0deg); }
+ 100% { -moz-transform: rotate(360deg); transform: rotate(360deg); }
+}
+@-o-keyframes loading-bar-spinner {
+ 0% { -o-transform: rotate(0deg); transform: rotate(0deg); }
+ 100% { -o-transform: rotate(360deg); transform: rotate(360deg); }
+}
+@-ms-keyframes loading-bar-spinner {
+ 0% { -ms-transform: rotate(0deg); transform: rotate(0deg); }
+ 100% { -ms-transform: rotate(360deg); transform: rotate(360deg); }
+}
+@keyframes loading-bar-spinner {
+ 0% { transform: rotate(0deg); transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); transform: rotate(360deg); }
+}
diff --git a/static/lib/loading-bar.js b/static/lib/loading-bar.js
new file mode 100755
index 000000000..5161dd883
--- /dev/null
+++ b/static/lib/loading-bar.js
@@ -0,0 +1,271 @@
+/*
+ * angular-loading-bar
+ *
+ * intercepts XHR requests and creates a loading bar.
+ * Based on the excellent nprogress work by rstacruz (more info in readme)
+ *
+ * (c) 2013 Wes Cruver
+ * License: MIT
+ */
+
+
+(function() {
+
+'use strict';
+
+// Alias the loading bar so it can be included using a simpler
+// (and maybe more professional) module name:
+angular.module('angular-loading-bar', ['chieffancypants.loadingBar']);
+
+
+/**
+ * loadingBarInterceptor service
+ *
+ * Registers itself as an Angular interceptor and listens for XHR requests.
+ */
+angular.module('chieffancypants.loadingBar', [])
+ .config(['$httpProvider', function ($httpProvider) {
+
+ var interceptor = ['$q', '$cacheFactory', 'cfpLoadingBar', function ($q, $cacheFactory, cfpLoadingBar) {
+
+ /**
+ * The total number of requests made
+ */
+ var reqsTotal = 0;
+
+ /**
+ * The number of requests completed (either successfully or not)
+ */
+ var reqsCompleted = 0;
+
+
+ /**
+ * calls cfpLoadingBar.complete() which removes the
+ * loading bar from the DOM.
+ */
+ function setComplete() {
+ cfpLoadingBar.complete();
+ reqsCompleted = 0;
+ reqsTotal = 0;
+ }
+
+ /**
+ * Determine if the response has already been cached
+ * @param {Object} config the config option from the request
+ * @return {Boolean} retrns true if cached, otherwise false
+ */
+ function isCached(config) {
+ var cache;
+ var defaults = $httpProvider.defaults;
+
+ if (config.method !== 'GET' || config.cache === false) {
+ config.cached = false;
+ return false;
+ }
+
+ if (config.cache === true && defaults.cache === undefined) {
+ cache = $cacheFactory.get('$http');
+ } else if (defaults.cache !== undefined) {
+ cache = defaults.cache;
+ } else {
+ cache = config.cache;
+ }
+
+ var cached = cache !== undefined ?
+ cache.get(config.url) !== undefined : false;
+
+ if (config.cached !== undefined && cached !== config.cached) {
+ return config.cached;
+ }
+ config.cached = cached;
+ return cached;
+ }
+
+ return {
+ 'request': function(config) {
+ // Check to make sure this request hasn't already been cached and that
+ // the requester didn't explicitly ask us to ignore this request:
+ if (!config.ignoreLoadingBar && !isCached(config)) {
+ if (reqsTotal === 0) {
+ cfpLoadingBar.start();
+ }
+ reqsTotal++;
+ }
+ return config;
+ },
+
+ 'response': function(response) {
+ if (!isCached(response.config)) {
+ reqsCompleted++;
+ if (reqsCompleted >= reqsTotal) {
+ setComplete();
+ } else {
+ cfpLoadingBar.set(reqsCompleted / reqsTotal);
+ }
+ }
+ return response;
+ },
+
+ 'responseError': function(rejection) {
+ if (!isCached(rejection.config)) {
+ reqsCompleted++;
+ if (reqsCompleted >= reqsTotal) {
+ setComplete();
+ } else {
+ cfpLoadingBar.set(reqsCompleted / reqsTotal);
+ }
+ }
+ return $q.reject(rejection);
+ }
+ };
+ }];
+
+ $httpProvider.interceptors.push(interceptor);
+ }])
+
+
+ /**
+ * Loading Bar
+ *
+ * This service handles adding and removing the actual element in the DOM.
+ * Generally, best practices for DOM manipulation is to take place in a
+ * directive, but because the element itself is injected in the DOM only upon
+ * XHR requests, and it's likely needed on every view, the best option is to
+ * use a service.
+ */
+ .provider('cfpLoadingBar', function() {
+
+ this.includeSpinner = true;
+ this.includeBar = true;
+ this.parentSelector = 'body';
+
+ this.$get = ['$document', '$timeout', '$animate', '$rootScope', function ($document, $timeout, $animate, $rootScope) {
+
+ var $parentSelector = this.parentSelector,
+ $parent = $document.find($parentSelector),
+ loadingBarContainer = angular.element('
'),
+ loadingBar = loadingBarContainer.find('div').eq(0),
+ spinner = angular.element('
');
+
+ var incTimeout,
+ completeTimeout,
+ started = false,
+ status = 0;
+
+ var includeSpinner = this.includeSpinner;
+ var includeBar = this.includeBar;
+
+ /**
+ * Inserts the loading bar element into the dom, and sets it to 2%
+ */
+ function _start() {
+ $timeout.cancel(completeTimeout);
+
+ // do not continually broadcast the started event:
+ if (started) {
+ return;
+ }
+
+ $rootScope.$broadcast('cfpLoadingBar:started');
+ started = true;
+
+ if (includeBar) {
+ $animate.enter(loadingBarContainer, $parent);
+ }
+
+ if (includeSpinner) {
+ $animate.enter(spinner, $parent);
+ }
+ _set(0.02);
+ }
+
+ /**
+ * Set the loading bar's width to a certain percent.
+ *
+ * @param n any value between 0 and 1
+ */
+ function _set(n) {
+ if (!started) {
+ return;
+ }
+ var pct = (n * 100) + '%';
+ loadingBar.css('width', pct);
+ status = n;
+
+ // increment loadingbar to give the illusion that there is always
+ // progress but make sure to cancel the previous timeouts so we don't
+ // have multiple incs running at the same time.
+ $timeout.cancel(incTimeout);
+ incTimeout = $timeout(function() {
+ _inc();
+ }, 250);
+ }
+
+ /**
+ * Increments the loading bar by a random amount
+ * but slows down as it progresses
+ */
+ function _inc() {
+ if (_status() >= 1) {
+ return;
+ }
+
+ var rnd = 0;
+
+ // TODO: do this mathmatically instead of through conditions
+
+ var stat = _status();
+ if (stat >= 0 && stat < 0.25) {
+ // Start out between 3 - 6% increments
+ rnd = (Math.random() * (5 - 3 + 1) + 3) / 100;
+ } else if (stat >= 0.25 && stat < 0.65) {
+ // increment between 0 - 3%
+ rnd = (Math.random() * 3) / 100;
+ } else if (stat >= 0.65 && stat < 0.9) {
+ // increment between 0 - 2%
+ rnd = (Math.random() * 2) / 100;
+ } else if (stat >= 0.9 && stat < 0.99) {
+ // finally, increment it .5 %
+ rnd = 0.005;
+ } else {
+ // after 99%, don't increment:
+ rnd = 0;
+ }
+
+ var pct = _status() + rnd;
+ _set(pct);
+ }
+
+ function _status() {
+ return status;
+ }
+
+ function _complete() {
+ $rootScope.$broadcast('cfpLoadingBar:completed');
+ _set(1);
+
+ // Attempt to aggregate any start/complete calls within 500ms:
+ completeTimeout = $timeout(function() {
+ $animate.leave(loadingBarContainer, function() {
+ status = 0;
+ started = false;
+ });
+ $animate.leave(spinner);
+ }, 500);
+ }
+
+ return {
+ start : _start,
+ set : _set,
+ status : _status,
+ inc : _inc,
+ complete : _complete,
+ includeSpinner : this.includeSpinner,
+ parentSelector : this.parentSelector
+ };
+
+
+
+ }]; //
+ }); // wtf javascript. srsly
+})(); //
diff --git a/static/partials/image-view.html b/static/partials/image-view.html
index 5d2753d8a..b2ea2d040 100644
--- a/static/partials/image-view.html
+++ b/static/partials/image-view.html
@@ -1,97 +1,89 @@
-
- No image found
-
+
+
+
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
- Full Image ID
-
-
-
-
-
-
Created
-
-
+
+
Created
+
+
-
-
+
+
-
-
-
-
-
-
-
- Showing {{(combinedChanges | filter:search | limitTo:50).length}} of {{(combinedChanges | filter:search).length}} results
-
-
-
-
-
-
-
-
- No matching changes
-
-
-
-
-
- {{folder}} / {{getFilename(change.file)}}
-
+
+
+
+
+
+
+
+ Showing {{(combinedChanges | filter:search | limitTo:50).length}} of {{(combinedChanges | filter:search).length}} results
+
+
+
+
+
+
+
+
+ No matching changes
+
+
+
+
+
+ {{folder}} / {{getFilename(change.file)}}
+
+
-
-
-
+
+
+
-
-
diff --git a/static/partials/landing.html b/static/partials/landing.html
index 714ccabe2..00594c00e 100644
--- a/static/partials/landing.html
+++ b/static/partials/landing.html
@@ -9,25 +9,28 @@
-
-
-
-
-
-
Top Repositories
-
-
-
{{repository.namespace}}/{{repository.name}}
-
+
+
+
-
-
-
You don't have access to any repositories in this organization yet.
-
You don't have any repositories yet!
-
@@ -41,8 +44,8 @@
diff --git a/static/partials/new-organization.html b/static/partials/new-organization.html
index de9927639..3033a20b7 100644
--- a/static/partials/new-organization.html
+++ b/static/partials/new-organization.html
@@ -1,8 +1,8 @@
-