diff --git a/endpoints/api.py b/endpoints/api.py index ed85e6e35..2a18edf4c 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -272,9 +272,10 @@ def get_matching_users(prefix): def get_matching_entities(prefix): teams = [] - namespace_name = request.args.get('namespace', None) + namespace_name = request.args.get('namespace', '') robot_namespace = None organization = None + try: organization = model.get_organization(namespace_name) @@ -308,7 +309,7 @@ def get_matching_entities(prefix): 'is_robot': user.is_robot, } - if user.is_org_member is not None: + if organization is not None: user_json['is_org_member'] = user.is_robot or user.is_org_member return user_json diff --git a/static/css/quay.css b/static/css/quay.css index 6f5842db8..f9dbb870f 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -3,6 +3,60 @@ margin: 0; } +.resource-view-element { + position: relative; +} + +.resource-view-element .resource-spinner { + z-index: 1; + position: absolute; + top: 10px; + left: 10px; + opacity: 0; + transition: opacity 0s ease-in-out; + text-align: center; +} + +.resource-view-element .resource-content { + visibility: hidden; +} + +.resource-view-element .resource-content.visible { + z-index: 2; + visibility: visible; +} + +.resource-view-element .resource-error { + margin: 10px; + font-size: 16px; + color: #444; + text-align: center; +} + +.resource-view-element .resource-spinner.visible { + opacity: 1; + transition: opacity 1s ease-in-out; +} + +.small-spinner { + display: inline-block; + width: 14px; + height: 14px; + border: solid 2px transparent; + border-top-color: #444; + border-left-color: #444; + 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; +} + +#loading-bar-spinner { + top: 70px; +} + .entity-search-element input { vertical-align: middle; } @@ -909,12 +963,20 @@ form input.ng-valid.ng-dirty, .entity-mini-listing { margin: 2px; white-space: nowrap !important; + position: relative; } .entity-mini-listing i { margin-right: 8px; } +.entity-mini-listing i.fa-exclamation-triangle { + position: absolute; + right: -16px; + top: 4px; + color: #c09853; +} + .entity-mini-listing .warning { margin-top: 6px; font-size: 10px; diff --git a/static/directives/billing-options.html b/static/directives/billing-options.html index 598586bcb..3b43e6805 100644 --- a/static/directives/billing-options.html +++ b/static/directives/billing-options.html @@ -5,7 +5,7 @@ Credit Card
- +
@@ -24,7 +24,7 @@
Billing Options - +
diff --git a/static/directives/entity-search.html b/static/directives/entity-search.html index fb4a6cf24..68f15b1cd 100644 --- a/static/directives/entity-search.html +++ b/static/directives/entity-search.html @@ -5,7 +5,7 @@
- +
diff --git a/static/directives/plan-manager.html b/static/directives/plan-manager.html index cf7612d0d..8ee324d20 100644 --- a/static/directives/plan-manager.html +++ b/static/directives/plan-manager.html @@ -1,6 +1,6 @@
- +
@@ -45,13 +45,13 @@
diff --git a/static/directives/repo-circle.html b/static/directives/repo-circle.html index 49d60ce56..336c03fe0 100644 --- a/static/directives/repo-circle.html +++ b/static/directives/repo-circle.html @@ -1,2 +1,2 @@ - + diff --git a/static/directives/resource-view.html b/static/directives/resource-view.html new file mode 100644 index 000000000..7763fa52a --- /dev/null +++ b/static/directives/resource-view.html @@ -0,0 +1,11 @@ +
+
+
+
+
+ {{ errorMessage }} +
+
+ +
+
diff --git a/static/directives/robots-manager.html b/static/directives/robots-manager.html index c478fbb02..68a2ed08a 100644 --- a/static/directives/robots-manager.html +++ b/static/directives/robots-manager.html @@ -1,5 +1,5 @@
- +
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 -
+
+
+
+ +

+ + {{repo.namespace}} + / + {{repo.name}} + / + {{image.value.id.substr(0, 12)}} +

+
-
- -
+ +
+ +
-
-
- -

- - {{repo.namespace}} - / - {{repo.name}} - / - {{image.id.substr(0, 12)}} -

-
- - -
- -
- - -
-
Full Image ID
-
-
-
-
- - - - + +
+
Full Image ID
+
+
+
+
+ + + + +
+
+ +
- - -
-
-
Created
-
-
+ +
Created
+
+ - -
- File Changes: -
-
- -
+ +
+ File Changes: +
+
+ +
- -
- -
-
-
-
- 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}} -
+ + +
+ +
+

Top Repositories

+
-
-
-
- You don't have access to any repositories in this organization yet. - You don't have any repositories yet! -
- Browse all repositories - Create a new repository + + +
+
+ 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 @@ -
- +
+
-
+
diff --git a/static/partials/new-repo.html b/static/partials/new-repo.html index 393d7003c..5f3ea1535 100644 --- a/static/partials/new-repo.html +++ b/static/partials/new-repo.html @@ -1,13 +1,15 @@
-

Please sign in

+
+
+
- +
- +
@@ -75,7 +77,7 @@ In order to make this repository private, you’ll need to upgrade your plan from {{ subscribedPlan.title }} to {{ planRequired.title }}. This will cost ${{ planRequired.price / 100 }}/month.
Upgrade now - +
diff --git a/static/partials/org-admin.html b/static/partials/org-admin.html index 17bdf56df..a4843005e 100644 --- a/static/partials/org-admin.html +++ b/static/partials/org-admin.html @@ -1,12 +1,5 @@ -
- -
- -
- No matching organization found -
- -
+
+
@@ -48,7 +41,7 @@
- +
@@ -105,8 +98,7 @@
- - +
diff --git a/static/partials/org-member-logs.html b/static/partials/org-member-logs.html index 7750de305..7f5d43077 100644 --- a/static/partials/org-member-logs.html +++ b/static/partials/org-member-logs.html @@ -1,16 +1,6 @@ -
- -
- -
- Organization not found -
- -
- Member not found -
- -
-
-
+
+
+
+
+
diff --git a/static/partials/org-view.html b/static/partials/org-view.html index 2a6deeb52..2a2dabdf2 100644 --- a/static/partials/org-view.html +++ b/static/partials/org-view.html @@ -1,51 +1,45 @@ -
- -
+
+
+
+
-
- No matching organization found -
+ + Create Team + -
-
-
- - - Create Team - - - Settings + Settings +
-
-