Work in progress: add a loading bar and convert to using the new ApiService and resource-view

This commit is contained in:
Joseph Schorr 2013-12-17 13:19:59 -05:00
parent a53106be3b
commit 414bd34d52
15 changed files with 1116 additions and 642 deletions

View file

@ -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;
}

View file

@ -0,0 +1,8 @@
<div class="container loading-status-element">
<div ng-show="hasError && !loading">
<span ng-transclude></span>
</div>
<div ng-show="loading">
Loading...
</div>
</div>

View file

@ -1,2 +1,2 @@
<i class="fa fa-lock fa-lg" style="{{ repo.is_public ? 'visibility: hidden' : 'visibility: visible' }}" title="Private Repository"></i>
<i class="fa fa-lock fa-lg" style="{{ repo.is_public ? 'visibility: hidden' : 'visibility: inherit' }}" title="Private Repository"></i>
<i class="fa fa-hdd"></i>

View file

@ -0,0 +1,11 @@
<div class="resource-view-element">
<div class="resource-spinner" ng-class="resource.loading ? 'visible' : ''">
<div class="small-spinner"></div>
</div>
<div class="resource-error" ng-show="!resource.loading && resource.hasError">
{{ errorMessage }}
</div>
<div class="resource-content" ng-class="(!resource.loading && !resource.hasError) ? 'visible' : ''">
<span ng-transclude></span>
</div>
</div>

View file

@ -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,7 +589,7 @@ 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/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}).
@ -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'
}

View file

@ -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,18 +24,14 @@ 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) {
document.location = '/user?plan=' + plan;
@ -61,61 +57,50 @@ function GuideCtrl($scope) {
function SecurityCtrl($scope) {
}
function RepoListCtrl($scope, Restangular, UserService) {
function RepoListCtrl($scope, Restangular, UserService, ApiService) {
$scope.namespace = null;
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
$scope.user = currentUser;
}, true);
// Monitor changes in the user.
UserService.updateUserIn($scope, function() {
loadMyRepos($scope.namespace);
});
// 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 loadPublicRepos = function() {
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);
$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,19 +236,131 @@ 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.
// Set the default tag.
$scope.setTag($routeParams.tag);
// 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();
// Fetch the image history.
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
@ -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);

102
static/lib/loading-bar.css Executable file
View file

@ -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); }
}

271
static/lib/loading-bar.js Executable file
View file

@ -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('<div id="loading-bar"><div class="bar"><div class="peg"></div></div></div>'),
loadingBar = loadingBarContainer.find('div').eq(0),
spinner = angular.element('<div id="loading-bar-spinner"><div class="spinner-icon"></div></div>');
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
})(); //

View file

@ -1,11 +1,7 @@
<div class="container" ng-show="!loading && !image">
<div class="loading-status" loading="loading" has-error="!image">
No image found
</div>
<div class="loading" ng-show="loading">
<i class="fa fa-spinner fa-spin fa-3x"></i>
</div>
<div class="container repo repo-image-view" ng-show="!loading && image">
<div class="header">
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="fa fa-chevron-left"></i></a>

View file

@ -9,19 +9,21 @@
</div>
<div ng-show="!user.anonymous">
<div ng-show="loadingmyrepos">
<i class="fa fa-spinner fa-spin fa-3x"></i>
</div>
<span class="namespace-selector" user="user" namespace="namespace" ng-show="!loadingmyrepos && user.organizations"></span>
<div ng-show="!loadingmyrepos && myrepos.length > 0">
<span class="namespace-selector" user="user" namespace="namespace" ng-show="user.organizations"></span>
<div class="resource-view" resource="my_repositories">
<!-- Repos -->
<div ng-show="my_repositories.value.length > 0">
<h2>Top Repositories</h2>
<div class="repo-listing" ng-repeat="repository in myrepos">
<div class="repo-listing" ng-repeat="repository in my_repositories.value">
<span class="repo-circle no-background" repo="repository"></span>
<a ng-href="/repository/{{ repository.namespace }}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
<div class="markdown-view description" content="repository.description" first-line-only="true"></div>
</div>
</div>
<div ng-show="!loadingmyrepos && myrepos.length == 0">
<!-- No Repos -->
<div ng-show="my_repositories.value.length == 0">
<div class="sub-message" style="margin-top: 20px">
<span ng-show="namespace != user.username">You don't have access to any repositories in this organization yet.</span>
<span ng-show="namespace == user.username">You don't have any repositories yet!</span>
@ -32,6 +34,7 @@
</div>
</div>
</div>
</div>
</div> <!-- col -->
<div class="col-md-4 col-md-offset-1">
@ -41,8 +44,8 @@
<div ng-show="!user.anonymous" class="user-welcome">
<img class="gravatar" src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=128&d=identicon" />
<div class="sub-message">Welcome <b>{{ user.username }}</b>!</div>
<a ng-show="myrepos" class="btn btn-primary" href="/repository/">Browse all repositories</a>
<a ng-show="myrepos" class="btn btn-success" href="/new/">Create a new repository</a>
<a ng-show="my_repositories.value" class="btn btn-primary" href="/repository/">Browse all repositories</a>
<a class="btn btn-success" href="/new/">Create a new repository</a>
</div>
</div> <!-- col -->
</div> <!-- row -->

View file

@ -82,7 +82,7 @@
<div class="user-setup" signed-in="signedIn()" redirect-url="'/plans/'"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal" ng-click="cancelNotedPlan()">Close</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->

View file

@ -1,13 +1,5 @@
<div class="loading" ng-show="loading">
<i class="fa fa-spinner fa-spin fa-3x"></i>
</div>
<div class="container" ng-show="!loading && (!repo || !permissions)">
No repository found
</div>
<div class="container repo repo-admin" ng-show="!loading && repo && permissions">
<div class="resource-view" resource="repository" error-message="'No repository found'"></div>
<div class="container repo repo-admin" ng-show="repo">
<div class="header row">
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="fa fa-chevron-left"></i></a>
<h3>
@ -156,12 +148,9 @@
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="URLs which will be invoked with an HTTP POST and JSON payload when a successful push to the repository occurs."></i>
</div>
<div class="panel-body" ng-show="webhooksLoading">
Loading webhooks: <i class="fa fa-spinner fa-spin fa-2x" style="vertical-align: middle; margin-left: 4px"></i>
</div>
<div class="panel-body" ng-show="!webhooksLoading">
<table class="permissions" ng-form="newWebhookForm">
<div class="panel-body" ng-form="newWebhookForm">
<div class="resource-view" resource="webhooksResource" error-message="'Could not load webhooks'">
<table class="permissions">
<thead>
<tr>
<td style="width: 500px;">Webhook URL</td>
@ -189,6 +178,7 @@
</tr>
</tbody>
</table>
</div>
<div class="right-info">
Quay will <b>POST</b> to these webhooks whenever a push occurs. See the <a href="/guide">User Guide</a> for more information.
@ -240,14 +230,18 @@
</div>
</div>
</div>
</div>
</div>
<div class="docker-auth-dialog" username="'$token'" token="shownToken.code"
<!-- Auth dialog -->
<div class="docker-auth-dialog" username="'$token'" token="shownToken.code"
shown="!!shownToken" counter="shownTokenCounter">
<i class="fa fa-key"></i> {{ shownToken.friendlyName }}
</div>
</div>
<!-- Modal message dialog -->
<div class="modal fade" id="cannotchangeModal">
<!-- Modal message dialog -->
<div class="modal fade" id="cannotchangeModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
@ -262,10 +256,10 @@
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="makepublicModal">
<!-- Modal message dialog -->
<div class="modal fade" id="makepublicModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
@ -284,11 +278,11 @@
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="makeprivateModal">
<!-- Modal message dialog -->
<div class="modal fade" id="makeprivateModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
@ -307,11 +301,11 @@
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="channgechangepermModal">
<!-- Modal message dialog -->
<div class="modal fade" id="channgechangepermModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
@ -327,11 +321,11 @@
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="confirmdeleteModal">
<!-- Modal message dialog -->
<div class="modal fade" id="confirmdeleteModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
@ -347,11 +341,11 @@
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="confirmaddoutsideModal">
<!-- Modal message dialog -->
<div class="modal fade" id="confirmaddoutsideModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
@ -367,5 +361,5 @@
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div><!-- /.modal -->

View file

@ -1,8 +1,4 @@
<div class="loading" ng-show="loading">
<i class="fa fa-spinner fa-spin fa-3x"></i>
</div>
<div class="container" ng-show="!loading">
<div class="container">
<div class="repo-list" ng-show="!user.anonymous">
<div ng-class="user.organizations.length ? 'section-header' : ''">
<div class="button-bar-right">
@ -27,30 +23,36 @@
<h3 ng-show="namespace == user.username">Your Repositories</h3>
<h3 ng-show="namespace != user.username">Repositories</h3>
<div ng-show="user_repositories.length > 0">
<div class="repo-listing" ng-repeat="repository in user_repositories">
<div class="resource-view" resource="user_repositories">
<!-- User/Org has repositories -->
<div ng-show="user_repositories.value.length > 0">
<div class="repo-listing" ng-repeat="repository in user_repositories.value">
<span class="repo-circle no-background" repo="repository"></span>
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
</div>
</div>
<div ng-show="user_repositories.length == 0" style="padding:20px;">
<!-- User/Org has no repositories -->
<div ng-show="user_repositories.value.length == 0" style="padding:20px;">
<div class="alert alert-info">
<h4 ng-show="namespace == user.username">You don't have any repositories yet!</h4>
<h4 ng-show="namespace != user.username">This organization doesn't have any repositories, or you have not been provided access.</h4>
<a href="/guide"><b>Click here</b> to learn how to create a repository</a>
</div>
</div>
</div>
</div>
<div class="repo-list">
<h3>Top Public Repositories</h3>
<div class="repo-listing" ng-repeat="repository in public_repositories">
<div class="resource-view" resource="public_repositories">
<div class="repo-listing" ng-repeat="repository in public_repositories.value">
<span class="repo-circle no-background" repo="repository"></span>
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
</div>
</div>
</div>
</div>

View file

@ -1,19 +1,10 @@
<div class="container" ng-show="!loading && !repo">
No repository found
</div>
<div class="loading" ng-show="loading">
<i class="fa fa-spinner fa-spin fa-3x"></i>
</div>
<div class="container repo" ng-show="!loading && repo">
<div class="resource-view" resource="repository" error-message="'No Repository Found'">
<div class="container repo">
<!-- Repo Header -->
<div class="header">
<h3>
<span class="repo-circle" repo="repo"></span>
<span style="color: #aaa;"> {{repo.namespace}}</span> <span style="color: #ccc">/</span> {{repo.name}}
<span class="settings-cog" ng-show="repo.can_admin" title="Repository Settings" bs-tooltip="tooltip.title" data-placement="bottom">
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/admin' }}">
<i class="fa fa-cog fa-lg"></i>
@ -64,12 +55,14 @@
<div class="empty-description" ng-show="repo.can_write">
To push images to this repository:<br><br>
<pre>sudo docker tag <i>0u123imageidgoeshere</i> quay.io/{{repo.namespace}}/{{repo.name}}
sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
</div>
</div>
<div class="repo-content" ng-show="!currentTag.image && repo.is_building">
<div class="empty-message">Your build is currently processing, if this takes longer than an hour, please contact <a href="mailto:support@quay.io">Quay.io support</a></div>
<div class="empty-message">
Your build is currently processing, if this takes longer than an hour, please contact <a href="mailto:support@quay.io">Quay.io support</a>
</div>
</div>
<!-- Content view -->
@ -79,7 +72,6 @@ sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
<div class="row">
<!-- Tree View container -->
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-heading">
<!-- Tag dropdown -->
@ -93,18 +85,14 @@ sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
</ul>
</div>
<span class="right-title">Tags</span>
</div>
<!-- Image history loading -->
<div ng-hide="imageHistory" style="padding: 10px; text-align: center;">
<i class="fa fa-spinner fa-spin fa-3x"></i>
</div>
<!-- Tree View itself -->
<!-- Image history tree -->
<div class="resource-view" resource="imageHistory">
<div id="image-history-container" onresize="tree.notifyResized()"></div>
</div>
</div>
</div>
<!-- Side Panel -->
<div class="col-md-4">
@ -112,10 +100,10 @@ sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
<div class="panel-heading">
<!-- Image dropdown -->
<div class="tag-dropdown dropdown" title="Images" bs-tooltip="tooltip.title" data-placement="top">
<i class="fa fa-archive"><span class="tag-count">{{imageHistory.length}}</span></i>
<i class="fa fa-archive"><span class="tag-count">{{imageHistory.value.length}}</span></i>
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">{{currentImage.id.substr(0, 12)}} <b class="caret"></b></a>
<ul class="dropdown-menu">
<li ng-repeat="image in imageHistory">
<li ng-repeat="image in imageHistory.value">
<a href="javascript:void(0)" ng-click="setImage(image)">{{image.id.substr(0, 12)}}</a>
</li>
</ul>
@ -139,10 +127,7 @@ sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
</dl>
<!-- Image changes loading -->
<div ng-hide="currentImageChanges">
<i class="fa fa-spinner fa-spin fa-3x"></i>
</div>
<div class="resource-view" resource="currentImageChangeResource">
<div class="changes-container small-changes-container"
ng-show="currentImageChanges.changed.length || currentImageChanges.added.length || currentImageChanges.removed.length">
<div class="changes-count-container accordion-toggle" data-toggle="collapse" data-parent="#accordion" data-target="#collapseChanges">
@ -179,7 +164,11 @@ sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
</div>
</div>
<div class="more-changes" ng-show="getMoreCount(currentImageChanges) > 0">
<a href="{{'/repository/' + repo.namespace + '/' + repo.name + '/image/' + currentImage.id}}">And {{getMoreCount(currentImageChanges)}} more...</a>
<a href="{{'/repository/' + repo.namespace + '/' + repo.name + '/image/' + currentImage.id}}">
And {{getMoreCount(currentImageChanges)}} more...
</a>
</div>
</div>
</div>
</div>
</div>

View file

@ -11,6 +11,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/static/lib/loading-bar.css">
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.0/css/font-awesome.css">
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.no-icons.min.css">
<link href='//fonts.googleapis.com/css?family=Droid+Sans:400,700' rel='stylesheet' type='text/css'>
@ -42,8 +44,9 @@
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular-route.min.js"></script>
<script src="//cdn.jsdelivr.net/underscorejs/1.5.2/underscore-min.js"></script>
<script src="//cdn.jsdelivr.net/restangular/1.1.3/restangular.js"></script>
<script src="//cdn.jsdelivr.net/restangular/1.2.0/restangular.min.js"></script>
<script src="static/lib/loading-bar.js"></script>
<script src="static/lib/angular-strap.min.js"></script>
<script src="static/lib/angulartics.js"></script>
<script src="static/lib/angulartics-mixpanel.js"></script>