From 414bd34d5262cde0d713ee6acb63a4b602e32a36 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 17 Dec 2013 13:19:59 -0500 Subject: [PATCH 1/3] Work in progress: add a loading bar and convert to using the new ApiService and resource-view --- static/css/quay.css | 54 +++ static/directives/loading-status.html | 8 + static/directives/repo-circle.html | 2 +- static/directives/resource-view.html | 11 + static/js/app.js | 81 ++++- static/js/controllers.js | 460 ++++++++++++-------------- static/lib/loading-bar.css | 102 ++++++ static/lib/loading-bar.js | 271 +++++++++++++++ static/partials/image-view.html | 6 +- static/partials/landing.html | 43 +-- static/partials/plans.html | 2 +- static/partials/repo-admin.html | 334 +++++++++---------- static/partials/repo-list.html | 54 +-- static/partials/view-repo.html | 325 +++++++++--------- templates/base.html | 5 +- 15 files changed, 1116 insertions(+), 642 deletions(-) create mode 100644 static/directives/loading-status.html create mode 100644 static/directives/resource-view.html create mode 100755 static/lib/loading-bar.css create mode 100755 static/lib/loading-bar.js diff --git a/static/css/quay.css b/static/css/quay.css index 6f5842db8..e005172c6 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; } diff --git a/static/directives/loading-status.html b/static/directives/loading-status.html new file mode 100644 index 000000000..9ed712459 --- /dev/null +++ b/static/directives/loading-status.html @@ -0,0 +1,8 @@ +
+
+ +
+
+ Loading... +
+
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/js/app.js b/static/js/app.js index 720a1fe5b..60ca1e082 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.mixpanel', '$strap.directives', 'ngCookies'], function($provide) { +quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'restangular', 'angularMoment', 'angulartics', 'angulartics.mixpanel', '$strap.directives', 'ngCookies'], function($provide, cfpLoadingBarProvider) { + cfpLoadingBarProvider.includeSpinner = false; + $provide.factory('UserService', ['Restangular', '$cookies', function(Restangular, $cookies) { var userResponse = { verified: false, @@ -106,6 +108,15 @@ quayApp = angular.module('quay', ['ngRoute', 'restangular', 'angularMoment', 'an return $cookies.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) { @@ -166,6 +177,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 = {} @@ -536,8 +589,8 @@ quayApp = angular.module('quay', ['ngRoute', 'restangular', 'angularMoment', 'an fixFooter: true, 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}). + 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', @@ -1245,6 +1298,24 @@ 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('organizationHeader', function () { var directiveDefinitionObject = { priority: 0, @@ -1508,14 +1579,14 @@ quayApp.directive('entitySearch', function () { number++; var input = $element[0].firstChild.firstChild; - $scope.namespace = $scope.namespace || ''; + var 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); + url += '?namespace=' + encodeURIComponent(namespace); if ($scope.includeTeams) { url += '&includeTeams=true' } diff --git a/static/js/controllers.js b/static/js/controllers.js index 196668b6f..bbe67a53e 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,63 +522,19 @@ 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); @@ -618,6 +549,44 @@ 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) { @@ -774,6 +743,7 @@ function ImageViewCtrl($scope, $routeParams, $rootScope, Restangular) { }; // Fetch the image. + $scope.loading = true; var imageFetch = Restangular.one('repository/' + namespace + '/' + name + '/image/' + imageid); imageFetch.get().then(function(image) { $scope.loading = false; @@ -1259,7 +1229,7 @@ function TeamViewCtrl($rootScope, $scope, Restangular, $routeParams) { function OrgsCtrl($scope, UserService) { $scope.loading = true; - $scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) { + $scope.$watch(function () { return UserService.currentUser(); }, function (currentUser) { $scope.user = currentUser; $scope.loading = false; }, true); 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..04dd9aabb 100644 --- a/static/partials/image-view.html +++ b/static/partials/image-view.html @@ -1,11 +1,7 @@ -
+
No image found
-
- -
-
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/plans.html b/static/partials/plans.html index 0a82c46a8..4b8e5a70e 100644 --- a/static/partials/plans.html +++ b/static/partials/plans.html @@ -82,7 +82,7 @@
diff --git a/static/partials/repo-admin.html b/static/partials/repo-admin.html index 50adf5744..a1bb36360 100644 --- a/static/partials/repo-admin.html +++ b/static/partials/repo-admin.html @@ -1,13 +1,5 @@ - -
- -
- -
- No repository found -
- -
+
+

@@ -35,7 +27,7 @@
- +
@@ -156,39 +148,37 @@
-
- Loading webhooks: -
- -
- - - - - - - - - - - - - - - - - - -
Webhook URL
{{ webhook.parameters.url }} - - - - -
- - - -
+
+
+ + + + + + + + + + + + + + + + + + +
Webhook URL
{{ webhook.parameters.url }} + + + + +
+ + + +
+
Quay will POST to these webhooks whenever a push occurs. See the User Guide for more information. @@ -240,132 +230,136 @@
- -
- {{ shownToken.friendlyName }} -
- - - - - - - - - - - - - - - - - - +

+
- - + +
+ {{ shownToken.friendlyName }} +
+ + + + + + + + + + + + + + + + + + + + + + diff --git a/static/partials/repo-list.html b/static/partials/repo-list.html index c8175e2ef..42f3aa5cc 100644 --- a/static/partials/repo-list.html +++ b/static/partials/repo-list.html @@ -1,8 +1,4 @@ -
- -
- -
+
@@ -26,31 +22,37 @@

Your Repositories

Repositories

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

You don't have any repositories yet!

+

This organization doesn't have any repositories, or you have not been provided access.

+ Click here to learn how to create a repository +
+
+
+ +
+ +
+

Top Public Repositories

+ - -
-
-

You don't have any repositories yet!

-

This organization doesn't have any repositories, or you have not been provided access.

- Click here to learn how to create a repository -
- -
-
- -
-

Top Public Repositories

-
diff --git a/static/partials/view-repo.html b/static/partials/view-repo.html index b2782c47d..c99ceb15c 100644 --- a/static/partials/view-repo.html +++ b/static/partials/view-repo.html @@ -1,191 +1,180 @@ -
- No repository found -
+
+
+ +
+

+ + {{repo.namespace}} / {{repo.name}} + + + + + +

+ + +