Merge remote-tracking branch 'origin/loading-bar'

Conflicts:
	static/js/app.js
This commit is contained in:
yackob03 2013-12-18 17:26:56 -05:00
commit ec01b47608
33 changed files with 2349 additions and 1113 deletions

View file

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

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

View file

@ -5,7 +5,7 @@
Credit Card
</div>
<div class="panel-body">
<i class="fa fa-spinner fa-spin fa-2x" ng-show="!currentCard || changingCard"></i>
<div class="quay-spinner" ng-show="!currentCard || changingCard"></div>
<div class="current-card" ng-show="currentCard && !changingCard">
<img src="{{ '/static/img/creditcards/' + getCreditImage(currentCard) }}" ng-show="currentCard.last4">
<span class="no-card-outline" ng-show="!currentCard.last4"></span>
@ -24,7 +24,7 @@
<div class="panel">
<div class="panel-title">
Billing Options
<i class="fa fa-spinner fa-spin" ng-show="working"></i>
<div class="quay-spinner" ng-show="working"></div>
</div>
<div class="panel-body">
<div class="settings-option">

View file

@ -5,7 +5,7 @@
<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu" aria-labelledby="entityDropdownMenu">
<li ng-show="lazyLoading"><i class="fa fa-spinner"></i></li>
<li ng-show="lazyLoading" style="padding: 10px"><div class="quay-spinner"></div></li>
<li role="presentation" ng-repeat="team in teams" ng-show="!lazyLoading"
ng-click="setEntity(team.name, 'team', false)">

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

@ -21,7 +21,7 @@
</div>
<div ng-show="loading">
<i class="fa fa-spinner fa-spin fa-3x"></i>
<div class="quay-spinner 3x"></div>
</div>
<div ng-show="!loading">
<div id="bar-chart" style="width: 800px; height: 500px;" ng-show="chartVisible">

View file

@ -1,6 +1,6 @@
<div class="plan-manager-element">
<!-- Loading/Changing -->
<i class="fa fa-spinner fa-spin fa-3x" ng-show="planLoading"></i>
<div class="quay-spinner 3x" ng-show="planLoading"></div>
<!-- Alerts -->
<div class="alert alert-danger" ng-show="limit == 'over' && !planLoading">
@ -45,13 +45,13 @@
<button class="btn" ng-show="subscribedPlan.stripeId !== plan.stripeId"
ng-class="subscribedPlan.price == 0 ? 'btn-primary' : 'btn-default'"
ng-click="changeSubscription(plan.stripeId)">
<i class="fa fa-spinner fa-spin" ng-show="planChanging"></i>
<span class="quay-spinner" ng-show="planChanging"></span>
<span ng-show="!planChanging && subscribedPlan.price != 0">Change</span>
<span ng-show="!planChanging && subscribedPlan.price == 0">Subscribe</span>
</button>
<button class="btn btn-danger" ng-show="subscription.plan === plan.stripeId && plan.price > 0"
ng-click="cancelSubscription()">
<i class="fa fa-spinner fa-spin" ng-show="planChanging"></i>
<span class="quay-spinner" ng-show="planChanging"></span>
<span ng-show="!planChanging">Cancel</span>
</button>
</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

@ -1,5 +1,5 @@
<div class="robots-manager-element">
<i class="fa fa-spinner fa-spin fa-3x" ng-show="loading"></i>
<div class="quay-spinner" ng-show="loading"></div>
<div class="alert alert-info">Robot accounts allow for delegating access in multiple repositories to role-based accounts that you manage</div>
<div class="container" ng-show="!loading">

View file

@ -19,7 +19,7 @@
</div>
</form>
<div ng-show="registering" style="text-align: center">
<i class="fa fa-spinner fa-spin fa-3x"></i>
<div class="quay-spinner 2x"></div>
</div>
<div ng-show="awaitingConfirmation">
<div class="sub-message">

View file

@ -0,0 +1 @@
<div class="small-spinner"></div>

View file

@ -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 += '<span class="name">' + datum.value + '</span>';
if (datum.entity.is_org_member !== undefined && !datum.entity.is_org_member && datum.kind == 'user') {
template += '<div class="alert-warning warning">This user is outside your organization</div>';
if (datum.entity.is_org_member === false && datum.entity.kind == 'user') {
template += '<i class="fa fa-exclamation-triangle" title="User is outside the organization"></i>';
}
template += '</div>';

File diff suppressed because it is too large Load diff

View file

@ -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] + ")");

View file

@ -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 <body> 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 = '<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);

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,97 +1,89 @@
<div class="container" ng-show="!loading && !image">
No image found
</div>
<div class="resource-view" resource="image" error-message="'No image found'">
<div class="container repo repo-image-view">
<div class="header">
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="fa fa-chevron-left"></i></a>
<h3>
<i class="fa fa-archive fa-lg" style="color: #aaa; margin-right: 10px;"></i>
<span style="color: #aaa;"> {{repo.namespace}}</span>
<span style="color: #ccc">/</span>
<span style="color: #666;">{{repo.name}}</span>
<span style="color: #ccc">/</span>
<span>{{image.value.id.substr(0, 12)}}</span>
</h3>
</div>
<div class="loading" ng-show="loading">
<i class="fa fa-spinner fa-spin fa-3x"></i>
</div>
<!-- Comment -->
<blockquote ng-show="image.value.comment">
<span class="markdown-view" content="image.value.comment"></span>
</blockquote>
<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>
<h3>
<i class="fa fa-archive fa-lg" style="color: #aaa; margin-right: 10px;"></i>
<span style="color: #aaa;"> {{repo.namespace}}</span>
<span style="color: #ccc">/</span>
<span style="color: #666;">{{repo.name}}</span>
<span style="color: #ccc">/</span>
<span>{{image.id.substr(0, 12)}}</span>
</h3>
</div>
<!-- Comment -->
<blockquote ng-show="image.comment">
<span class="markdown-view" content="image.comment"></span>
</blockquote>
<!-- Information -->
<dl class="dl-normal">
<dt>Full Image ID</dt>
<dd>
<div>
<div class="id-container">
<div class="input-group">
<input id="full-id" type="text" class="form-control" value="{{ image.id }}" readonly>
<span id="copyClipboard" class="input-group-addon" title="Copy to Clipboard" data-clipboard-target="full-id">
<i class="fa fa-copy"></i>
</span>
<!-- Information -->
<dl class="dl-normal">
<dt>Full Image ID</dt>
<dd>
<div>
<div class="id-container">
<div class="input-group">
<input id="full-id" type="text" class="form-control" value="{{ image.value.id }}" readonly>
<span id="copyClipboard" class="input-group-addon" title="Copy to Clipboard" data-clipboard-target="full-id">
<i class="fa fa-copy"></i>
</span>
</div>
</div>
<div id="clipboardCopied" style="display: none">
Copied to clipboard
</div>
</div>
<div id="clipboardCopied" style="display: none">
Copied to clipboard
</div>
</div>
</dd>
<dt>Created</dt>
<dd am-time-ago="parseDate(image.created)"></dd>
</dl>
</dd>
<dt>Created</dt>
<dd am-time-ago="parseDate(image.created)"></dd>
</dl>
<!-- Changes tabs -->
<div ng-show="combinedChanges.length > 0">
<b>File Changes:</b>
<br>
<br>
<ul class="nav nav-tabs">
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#filterable">Filterable View</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#tree" ng-click="initializeTree()">Tree View</a></li>
</ul>
</div>
<!-- Changes tabs -->
<div ng-show="combinedChanges.length > 0">
<b>File Changes:</b>
<br>
<br>
<ul class="nav nav-tabs">
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#filterable">Filterable View</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#tree" ng-click="initializeTree()">Tree View</a></li>
</ul>
</div>
<!-- Changes tab content -->
<div class="tab-content" ng-show="combinedChanges.length > 0">
<!-- Filterable view -->
<div class="tab-pane active" id="filterable">
<div class="changes-container full-changes-container">
<div class="change-side-controls">
<div class="result-count">
Showing {{(combinedChanges | filter:search | limitTo:50).length}} of {{(combinedChanges | filter:search).length}} results
</div>
<div class="filter-input">
<input id="change-filter" class="form-control" placeholder="Filter Changes" type="text" ng-model="search.$">
</div>
</div>
<div style="height: 28px;"></div>
<div class="changes-list well well-sm">
<div ng-show="(combinedChanges | filter:search | limitTo:1).length == 0">
No matching changes
</div>
<div class="change" ng-repeat="change in combinedChanges | filter:search | limitTo:50">
<i ng-class="{'added': 'fa fa-plus-square', 'removed': 'fa fa-minus-square', 'changed': 'fa fa-pencil-square'}[change.kind]"></i>
<span title="{{change.file}}">
<span style="color: #888;">
<span ng-repeat="folder in getFolders(change.file)"><a href="javascript:void(0)" ng-click="setFolderFilter(getFolder(change.file), $index)">{{folder}}</a>/</span></span><span>{{getFilename(change.file)}}</span>
</span>
<!-- Changes tab content -->
<div class="tab-content" ng-show="combinedChanges.length > 0">
<!-- Filterable view -->
<div class="tab-pane active" id="filterable">
<div class="changes-container full-changes-container">
<div class="change-side-controls">
<div class="result-count">
Showing {{(combinedChanges | filter:search | limitTo:50).length}} of {{(combinedChanges | filter:search).length}} results
</div>
<div class="filter-input">
<input id="change-filter" class="form-control" placeholder="Filter Changes" type="text" ng-model="search.$">
</div>
</div>
<div style="height: 28px;"></div>
<div class="changes-list well well-sm">
<div ng-show="(combinedChanges | filter:search | limitTo:1).length == 0">
No matching changes
</div>
<div class="change" ng-repeat="change in combinedChanges | filter:search | limitTo:50">
<i ng-class="{'added': 'fa fa-plus-square', 'removed': 'fa fa-minus-square', 'changed': 'fa fa-pencil-square'}[change.kind]"></i>
<span title="{{change.file}}">
<span style="color: #888;">
<span ng-repeat="folder in getFolders(change.file)"><a href="javascript:void(0)" ng-click="setFolderFilter(getFolder(change.file), $index)">{{folder}}</a>/</span></span><span>{{getFilename(change.file)}}</span>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Tree view -->
<div class="tab-pane" id="tree">
<div id="changes-tree-container" class="changes-container" onresize="tree && tree.notifyResized()"></div>
</div>
<!-- Tree view -->
<div class="tab-pane" id="tree">
<div id="changes-tree-container" class="changes-container" onresize="tree && tree.notifyResized()"></div>
</div>
</div>
</div>
</div>

View file

@ -9,25 +9,28 @@
</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">
<h2>Top Repositories</h2>
<div class="repo-listing" ng-repeat="repository in myrepos">
<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>
<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 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>
<div ng-show="!loadingmyrepos && myrepos.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>
<div class="options">
<a class="btn btn-primary" href="/repository/">Browse all repositories</a>
<a class="btn btn-success" href="/new/" ng-show="canCreateRepo(namespace)">Create a new repository</a>
<!-- 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>
<div class="options">
<a class="btn btn-primary" href="/repository/">Browse all repositories</a>
<a class="btn btn-success" href="/new/" ng-show="canCreateRepo(namespace)">Create a new repository</a>
</div>
</div>
</div>
</div>
@ -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

@ -1,8 +1,8 @@
<div class="loading" ng-show="loading || creating">
<i class="fa fa-spinner fa-spin fa-3x"></i>
<div class="loading" ng-show="creating">
<div class="quay-spinner"></div>
</div>
<div class="container create-org" ng-show="!loading && !creating">
<div class="container create-org" ng-show="!creating">
<div class="row header-row">
<div class="col-md-8 col-md-offset-1">

View file

@ -1,13 +1,15 @@
<div class="container" ng-show="user.anonymous">
<h3>Please <a href="/signin/">sign in</a></h3>
<div class="col-sm-6 col-sm-offset-3">
<div class="user-setup" redirect-url="'/new/'"></div>
</div>
</div>
<div class="container" ng-show="!user.anonymous && building">
<i class="fa fa-spinner fa-spin fa-3x"></i>
<div class="quay-spinner"></div>
</div>
<div class="container" ng-show="!user.anonymous && creating">
<i class="fa fa-spinner fa-spin fa-3x"></i>
<div class="quay-spinner"></div>
</div>
<div class="container" ng-show="!user.anonymous && uploading">
@ -75,7 +77,7 @@
In order to make this repository private, youll need to upgrade your plan from <b>{{ subscribedPlan.title }}</b> to <b>{{ planRequired.title }}</b>. This will cost $<span>{{ planRequired.price / 100 }}</span>/month.
</div>
<a class="btn btn-primary" ng-click="upgradePlan()" ng-show="!planChanging">Upgrade now</a>
<i class="fa fa-spinner fa-spin fa-3x" ng-show="planChanging"></i>
<div class="quay-spinner" ng-show="planChanging"></div>
</div>
<div class="required-plan" ng-show="repo.is_public == '0' && planRequired && !isUserNamespace">

View file

@ -1,12 +1,5 @@
<div class="loading" ng-show="loading">
<i class="fa fa-spinner fa-spin fa-3x"></i>
</div>
<div class="loading" ng-show="!loading && !organization">
No matching organization found
</div>
<div class="org-admin container" ng-show="!loading && organization">
<div class="resource-view" resource="orgResource" error-message="'No organization found'"></div>
<div class="org-admin container" ng-show="organization">
<div class="organization-header" organization="organization" clickable="true"></div>
<div class="row">
@ -48,7 +41,7 @@
<!-- Billing History tab -->
<div id="billing" class="tab-pane">
<div ng-show="invoiceLoading">
<i class="fa fa-spinner fa-spin fa-3x"></i>
<div class="quay-spinner"></div>
</div>
<div ng-show="!invoiceLoading && !invoices">
@ -105,8 +98,7 @@
<!-- Members tab -->
<div id="members" class="tab-pane">
<i class="fa fa-spinner fa-spin fa-3x" ng-show="membersLoading"></i>
<div class="quay-spinner" ng-show="membersLoading"></div>
<div ng-show="!membersLoading">
<div class="side-controls">
<div class="result-count">

View file

@ -1,16 +1,6 @@
<div class="org-member-logs container" ng-show="loading">
<i class="fa fa-spinner fa-spin fa-3x"></i>
</div>
<div class="container" ng-show="!loading && !organization">
Organization not found
</div>
<div class="container" ng-show="!loading && !memberInfo">
Member not found
</div>
<div class="org-member-logs container" ng-show="!loading && organization && memberInfo">
<div class="organization-header" organization="organization" clickable="true"></div>
<div class="logs-view" organization="organization" performer="memberInfo" visible="organization && memberInfo && ready"></div>
<div class="resource-view" resource="memberResource" error-message="'Member not found'">
<div class="org-member-logs container">
<div class="organization-header" organization="organization" clickable="true"></div>
<div class="logs-view" organization="organization" performer="memberInfo" visible="organization && memberInfo && ready"></div>
</div>
</div>

View file

@ -1,51 +1,45 @@
<div class="loading" ng-show="loading">
<i class="fa fa-spinner fa-spin fa-3x"></i>
</div>
<div class="resource-view" resource="orgResource" error-message="'No matching organization found'">
<div class="org-view container">
<div class="organization-header" organization="organization">
<div class="header-buttons" ng-show="organization.is_admin">
<div class="loading" ng-show="!loading && !organization">
No matching organization found
</div>
<span class="popup-input-button" pattern="TEAM_PATTERN" placeholder="'Team Name'"
submitted="createTeam(value)">
<i class="fa fa-group"></i> Create Team
</span>
<div class="org-view container" ng-show="!loading && organization">
<div class="organization-header" organization="organization">
<div class="header-buttons" ng-show="organization.is_admin">
<span class="popup-input-button" pattern="TEAM_PATTERN" placeholder="'Team Name'"
submitted="createTeam(value)">
<i class="fa fa-group"></i> Create Team
</span>
<a class="btn btn-default" href="/organization/{{ organization.name }}/admin"><i class="fa fa-gear"></i> Settings</a>
<a class="btn btn-default" href="/organization/{{ organization.name }}/admin"><i class="fa fa-gear"></i> Settings</a>
</div>
</div>
</div>
<div class="row hidden-xs">
<div class="col-md-4 col-md-offset-8 col-sm-5 col-sm-offset-7 header-col" ng-show="organization.is_admin">
Team Permissions
<i class="info-icon fa fa-info-circle" data-placement="bottom" data-original-title="" title=""
data-content="Global permissions for the team and its members<br><br><dl><dt>Member</dt><dd>Permissions are assigned on a per repository basis</dd><dt>Creator</dt><dd>A team can create its own repositories</dd><dt>Admin</dt><dd>A team has full control of the organization</dd></dl>"></i>
<div class="row hidden-xs">
<div class="col-md-4 col-md-offset-8 col-sm-5 col-sm-offset-7 header-col" ng-show="organization.is_admin">
Team Permissions
<i class="info-icon fa fa-info-circle" data-placement="bottom" data-original-title="" title=""
data-content="Global permissions for the team and its members<br><br><dl><dt>Member</dt><dd>Permissions are assigned on a per repository basis</dd><dt>Creator</dt><dd>A team can create its own repositories</dd><dt>Admin</dt><dd>A team has full control of the organization</dd></dl>"></i>
</div>
</div>
</div>
<div class="team-listing" ng-repeat="(name, team) in organization.teams">
<div id="team-{{name}}" class="row">
<div class="col-sm-7 col-md-8">
<div class="team-title">
<i class="fa fa-group"></i>
<span ng-show="team.can_view">
<a href="/organization/{{ organization.name }}/teams/{{ team.name }}">{{ team.name }}</a>
</span>
<span ng-show="!team.can_view">
{{ team.name }}
</span>
<div class="team-listing" ng-repeat="(name, team) in organization.teams">
<div id="team-{{name}}" class="row">
<div class="col-sm-7 col-md-8">
<div class="team-title">
<i class="fa fa-group"></i>
<span ng-show="team.can_view">
<a href="/organization/{{ organization.name }}/teams/{{ team.name }}">{{ team.name }}</a>
</span>
<span ng-show="!team.can_view">
{{ team.name }}
</span>
</div>
<div class="team-description markdown-view" content="team.description" first-line-only="true"></div>
</div>
<div class="team-description markdown-view" content="team.description" first-line-only="true"></div>
</div>
<div class="col-sm-5 col-md-4 control-col" ng-show="organization.is_admin">
<span class="role-group" current-role="team.role" role-changed="setRole(role, team.name)" roles="teamRoles"></span>
<button class="btn btn-sm btn-danger" ng-click="askDeleteTeam(team.name)">Delete</button>
<div class="col-sm-5 col-md-4 control-col" ng-show="organization.is_admin">
<span class="role-group" current-role="team.role" role-changed="setRole(role, team.name)" roles="teamRoles"></span>
<button class="btn btn-sm btn-danger" ng-click="askDeleteTeam(team.name)">Delete</button>
</div>
</div>
</div>
</div>

View file

@ -1,6 +1,6 @@
<div class="container org-list conntent-container">
<div class="loading" ng-show="loading">
<i class="fa fa-spinner fa-spin fa-3x"></i>
<div class="loading" ng-show="!user">
<div class="quay-spinner"></div>
</div>
<div class="button-bar-right">

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>
@ -35,7 +27,7 @@
<div id="logs" class="tab-pane">
<div class="logs-view" repository="repo" visible="logsShown"></div>
</div>
<!-- Permissions tab -->
<div id="permissions" class="tab-pane active">
<!-- User Access Permissions -->
@ -137,7 +129,7 @@
<tr>
<td class="admin-search">
<input type="text" class="form-control" placeholder="New token description" ng-model="newToken.friendlyName"required>
<input type="text" class="form-control" placeholder="New token description" ng-model="newToken.friendlyName" required>
</td>
<td class="admin-search">
<button type="submit" ng-disabled="createTokenForm.$invalid" class="btn btn-sm btn-default">Create</button>
@ -156,39 +148,44 @@
<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">
<div class="resource-view" resource="webhooksResource" error-message="'Could not load webhooks'">
<table class="permissions">
<thead>
<tr>
<td style="width: 500px;">Webhook URL</td>
<td></td>
</tr>
</thead>
<tbody>
<tr ng-repeat="webhook in webhooks">
<td>{{ webhook.parameters.url }}</td>
<td>
<span class="delete-ui" tabindex="0">
<span class="delete-ui-button" ng-click="deleteWebhook(webhook)"><button class="btn btn-danger">Delete</button></span>
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="right" title="Delete Webhook"></i>
</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="panel-body" ng-show="!webhooksLoading">
<table class="permissions" ng-form="newWebhookForm">
<thead>
<tr>
<td style="width: 500px;">Webhook URL</td>
<td></td>
</tr>
</thead>
<tbody>
<tr ng-repeat="webhook in webhooks">
<td>{{ webhook.parameters.url }}</td>
<td>
<span class="delete-ui" tabindex="0">
<span class="delete-ui-button" ng-click="deleteWebhook(webhook)"><button class="btn btn-danger">Delete</button></span>
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="right" title="Delete Webhook"></i>
</span>
</td>
</tr>
<tr>
<td>
<input type="url" class="form-control" placeholder="New webhook url..." ng-model="newWebhook.url" required>
</td>
<td>
<button class="btn btn-primary" type="submit" ng-click="createWebhook()">Create</button>
</td>
</tr>
</tbody>
</table>
<form name="createWebhookForm" ng-submit="createWebhook()">
<table class="permissions">
<tbody>
<tr>
<td style="width: 500px;">
<input type="url" class="form-control" placeholder="New webhook url..." ng-model="newWebhook.url" required>
</td>
<td>
<button class="btn btn-primary" type="submit" ng-disabled="createWebhookForm.$invalid">Create</button>
</td>
</tr>
</tbody>
</table>
</form>
<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,132 +237,136 @@
</div>
</div>
</div>
<div class="docker-auth-dialog" username="'$token'" token="shownToken.code"
shown="!!shownToken" counter="shownTokenCounter">
<i class="fa fa-key"></i> {{ shownToken.friendlyName }}
</div>
<!-- Modal message dialog -->
<div class="modal fade" id="cannotchangeModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Cannot change</h4>
</div>
<div class="modal-body">
The selected action could not be performed because you do not have that authority.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="makepublicModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Make Repository Public</h4>
</div>
<div class="modal-body">
<div class="alert alert-warning">
Warning: This will allow <b>anyone</b> to pull from this repository
</div>
Are you sure you want to make this repository public?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" ng-click="changeAccess('public')">Make Public</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="makeprivateModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Make Repository Private</h4>
</div>
<div class="modal-body">
<div class="alert alert-warning">
Warning: Only users on the permissions list will be able to access this repository.
</div>
Are you sure you want to make this repository private?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success" ng-click="changeAccess('private')">Make Private</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="channgechangepermModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Cannot change permissions</h4>
</div>
<div class="modal-body">
<span ng-show="!changePermError">You do not have permission to change the permissions on the repository.</span>
<span ng-show="changePermError">{{ changePermError }}</span>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="confirmdeleteModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Delete Repository?</h4>
</div>
<div class="modal-body">
Are you <b>absolutely, positively</b> sure you would like to delete this repository? This <b>cannot be undone</b>.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" ng-click="deleteRepo()">Delete Repository</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div>
</div>
<!-- Modal message dialog -->
<div class="modal fade" id="confirmaddoutsideModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Add User?</h4>
</div>
<div class="modal-body">
The selected user is outside of your organization. Are you sure you want to grant the user access to this repository?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="grantRole()">Yes, I'm sure</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- 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>
<!-- Modal message dialog -->
<div class="modal fade" id="cannotchangeModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Cannot change</h4>
</div>
<div class="modal-body">
The selected action could not be performed because you do not have that authority.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="makepublicModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Make Repository Public</h4>
</div>
<div class="modal-body">
<div class="alert alert-warning">
Warning: This will allow <b>anyone</b> to pull from this repository
</div>
Are you sure you want to make this repository public?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" ng-click="changeAccess('public')">Make Public</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="makeprivateModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Make Repository Private</h4>
</div>
<div class="modal-body">
<div class="alert alert-warning">
Warning: Only users on the permissions list will be able to access this repository.
</div>
Are you sure you want to make this repository private?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success" ng-click="changeAccess('private')">Make Private</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="channgechangepermModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Cannot change permissions</h4>
</div>
<div class="modal-body">
<span ng-show="!changePermError">You do not have permission to change the permissions on the repository.</span>
<span ng-show="changePermError">{{ changePermError }}</span>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="confirmdeleteModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Delete Repository?</h4>
</div>
<div class="modal-body">
Are you <b>absolutely, positively</b> sure you would like to delete this repository? This <b>cannot be undone</b>.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" ng-click="deleteRepo()">Delete Repository</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="confirmaddoutsideModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Add User?</h4>
</div>
<div class="modal-body">
The selected user is outside of your organization. Are you sure you want to grant the user access to this repository?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="grantRole()">Yes, I'm sure</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</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">
@ -26,31 +22,37 @@
<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>
<!-- 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="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 ng-show="user_repositories.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 class="repo-list">
<h3>Top Public Repositories</h3>
<div class="repo-listing" ng-repeat="repository in public_repositories">
<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>

View file

@ -1,47 +1,42 @@
<div class="loading" ng-show="loading">
<i class="fa fa-spinner fa-spin fa-3x"></i>
</div>
<div class="resource-view" resource="orgResource" error-message="'No matching organization'">
<div class="team-view container">
<div class="organization-header" organization="organization" team-name="teamname"></div>
<div class="resource-view" resource="membersResource" error-message="'No matching team found'">
<div class="description markdown-input" content="team.description" can-write="organization.is_admin"
content-changed="updateForDescription" field-title="'team description'"></div>
<div class="loading" ng-show="!loading && !organization">
No matching team found
</div>
<div class="team-view container" ng-show="!loading && organization">
<div class="organization-header" organization="organization" team-name="teamname"></div>
<div class="description markdown-input" content="team.description" can-write="organization.is_admin"
content-changed="updateForDescription" field-title="'team description'"></div>
<div class="panel panel-default">
<div class="panel-heading">Team Members
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="Users that inherit all permissions delegated to this team"></i>
</div>
<div class="panel-body">
<table class="permissions">
<tr ng-repeat="(name, member) in members">
<td class="user entity">
<span class="entity-reference" name="member.username" isrobot="member.is_robot"></span>
</td>
<td>
<span class="delete-ui" tabindex="0" title="Remove User" ng-show="canEditMembers">
<span class="delete-ui-button" ng-click="removeMember(member.username)"><button class="btn btn-danger">Remove</button></span>
<i class="fa fa-times"></i>
</span>
</td>
</tr>
<tr ng-show="canEditMembers">
<td colspan="2">
<span class="entity-search" namespace="orgname" include-teams="false" input-title="'Add a user...'"
entity-selected="addNewMember" is-organization="true"></span>
</td>
</tr>
</table>
<div class="panel panel-default">
<div class="panel-heading">Team Members
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="Users that inherit all permissions delegated to this team"></i>
</div>
<div class="panel-body">
<table class="permissions">
<tr ng-repeat="(name, member) in members">
<td class="user entity">
<span class="entity-reference" name="member.username" isrobot="member.is_robot"></span>
</td>
<td>
<span class="delete-ui" tabindex="0" title="Remove User" ng-show="canEditMembers">
<span class="delete-ui-button" ng-click="removeMember(member.username)"><button class="btn btn-danger">Remove</button></span>
<i class="fa fa-times"></i>
</span>
</td>
</tr>
<tr ng-show="canEditMembers">
<td colspan="2">
<span class="entity-search" namespace="orgname" include-teams="false" input-title="'Add a user...'"
entity-selected="addNewMember" is-organization="true"></span>
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Modal message dialog -->
<div class="modal fade" id="cannotChangeTeamModal">
<div class="modal-dialog">

View file

@ -1,12 +1,12 @@
<div class="loading" ng-show="loading">
<i class="fa fa-spinner fa-spin fa-3x"></i>
<div class="loading" ng-show="!user">
<div class="quay-spinner"></div>
</div>
<div class="loading" ng-show="!loading && !user">
<div class="loading" ng-show="user.anonymous">
No matching user found
</div>
<div class="user-admin container" ng-show="!loading && user">
<div class="user-admin container" ng-show="!user.anonymous">
<div class="row">
<div class="organization-header-element">
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=24&amp;d=identicon">
@ -45,18 +45,21 @@
<!-- Change password tab -->
<div id="password" class="tab-pane">
<div class="loading" ng-show="updatingUser">
<i class="fa fa-spinner fa-spin fa-3x"></i>
<div class="quay-spinner 3x"></div>
</div>
<div class="row">
<form class="form-change-pw col-md-6" name="changePasswordForm" ng-submit="changePassword()" data-trigger="manual"
data-content="{{ changePasswordError }}" data-placement="right" ng-show="!awaitingConfirmation && !registering">
<input type="password" class="form-control" placeholder="Your new password" ng-model="user.password" required>
<input type="password" class="form-control" placeholder="Verify your new password" ng-model="user.repeatPassword"
match="user.password" required>
<button class="btn btn-danger" ng-disabled="changePasswordForm.$invalid" type="submit"
analytics-on analytics-event="register">Change Password</button>
<span class="help-block" ng-show="changePasswordSuccess">Password changed successfully</span>
</form>
<span class="help-block" ng-show="changePasswordSuccess">Password changed successfully</span>
<div ng-show="!updatingUser">
<form class="form-change-pw col-md-6" name="changePasswordForm" ng-submit="changePassword()" data-trigger="manual"
data-content="{{ changePasswordError }}" data-placement="right" ng-show="!awaitingConfirmation && !registering">
<input type="password" class="form-control" placeholder="Your new password" ng-model="user.password" required>
<input type="password" class="form-control" placeholder="Verify your new password" ng-model="user.repeatPassword"
match="user.password" required>
<button class="btn btn-danger" ng-disabled="changePasswordForm.$invalid" type="submit"
analytics-on analytics-event="change-pass">Change Password</button>
</form>
</div>
</div>
</div>

View file

@ -1,191 +1,180 @@
<div class="container" ng-show="!loading && !repo">
No repository found
</div>
<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>
</a>
</span>
</h3>
<!-- Pull command -->
<div class="pull-command visible-md visible-lg" style="display: none;">
<span class="pull-command-title">Pull repository:</span>
<div class="pull-container">
<div class="input-group">
<input id="pull-text" type="text" class="form-control" value="{{ 'docker pull quay.io/' + repo.namespace + '/' + repo.name }}" readonly>
<span id="copyClipboard" class="input-group-addon" title="Copy to Clipboard" data-clipboard-target="pull-text">
<i class="fa fa-copy"></i>
</span>
</div>
</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">
<!-- 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>
</a>
</span>
</h3>
<!-- Pull command -->
<div class="pull-command visible-md visible-lg" style="display: none;">
<span class="pull-command-title">Pull repository:</span>
<div class="pull-container">
<div class="input-group">
<input id="pull-text" type="text" class="form-control" value="{{ 'docker pull quay.io/' + repo.namespace + '/' + repo.name }}" readonly>
<span id="copyClipboard" class="input-group-addon" title="Copy to Clipboard" data-clipboard-target="pull-text">
<i class="fa fa-copy"></i>
</span>
<div id="clipboardCopied" class="hovering" style="display: none">
Copied to clipboard
</div>
</div>
</div>
<div id="clipboardCopied" class="hovering" style="display: none">
Copied to clipboard
<!-- Status boxes -->
<div class="status-boxes">
<div id="buildInfoBox" class="status-box" ng-show="repo.is_building"
bs-popover="'static/partials/build-status-item.html'" data-placement="bottom">
<span class="title">
<span class="quay-spinner"></span>
<b>Building Images</b>
</span>
<span class="count" ng-class="buildsInfo ? 'visible' : ''"><span>{{ buildsInfo ? buildsInfo.length : '-' }}</span></span>
</div>
</div>
</div>
<!-- Status boxes -->
<div class="status-boxes">
<div id="buildInfoBox" class="status-box" ng-show="repo.is_building"
bs-popover="'static/partials/build-status-item.html'" data-placement="bottom">
<span class="title">
<i class="fa fa-spinner fa-spin"></i>
<b>Building Images</b>
</span>
<span class="count" ng-class="buildsInfo ? 'visible' : ''"><span>{{ buildsInfo ? buildsInfo.length : '-' }}</span></span>
</div>
</div>
<!-- Description -->
<div class="description markdown-input" content="repo.description" can-write="repo.can_write"
content-changed="updateForDescription" field-title="'repository description'"></div>
<!-- Description -->
<div class="description markdown-input" content="repo.description" can-write="repo.can_write"
content-changed="updateForDescription" field-title="'repository description'"></div>
<!-- Empty message -->
<div class="repo-content" ng-show="!currentTag.image && !repo.is_building">
<div class="empty-message">
This repository is empty
</div>
<!-- Empty message -->
<div class="repo-content" ng-show="!currentTag.image && !repo.is_building">
<div class="empty-message">
This repository is empty
</div>
<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}}
<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>
</div>
</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>
<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>
<!-- Content view -->
<div class="repo-content" ng-show="currentTag.image">
<!-- Image History -->
<div id="image-history" style="max-height: 10px;">
<div class="row">
<!-- Tree View container -->
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-heading">
<!-- Tag dropdown -->
<div class="tag-dropdown dropdown" title="Tags" bs-tooltip="tooltip.title" data-placement="top">
<i class="fa fa-tag"><span class="tag-count">{{getTagCount(repo)}}</span></i>
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">{{currentTag.name}} <b class="caret"></b></a>
<ul class="dropdown-menu">
<li ng-repeat="tag in repo.tags">
<a href="javascript:void(0)" ng-click="setTag(tag.name)">{{tag.name}}</a>
</li>
</ul>
<!-- Content view -->
<div class="repo-content" ng-show="currentTag.image">
<!-- Image History -->
<div id="image-history" style="max-height: 10px;">
<div class="row">
<!-- Tree View container -->
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-heading">
<!-- Tag dropdown -->
<div class="tag-dropdown dropdown" title="Tags" bs-tooltip="tooltip.title" data-placement="top">
<i class="fa fa-tag"><span class="tag-count">{{getTagCount(repo)}}</span></i>
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">{{currentTag.name}} <b class="caret"></b></a>
<ul class="dropdown-menu">
<li ng-repeat="tag in repo.tags">
<a href="javascript:void(0)" ng-click="setTag(tag.name)">{{tag.name}}</a>
</li>
</ul>
</div>
<span class="right-title">Tags</span>
</div>
<!-- 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">
<div class="panel panel-default">
<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.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.value">
<a href="javascript:void(0)" ng-click="setImage(image)">{{image.id.substr(0, 12)}}</a>
</li>
</ul>
</div>
<span class="right-title">Image</span>
</div>
<span class="right-title">Tags</span>
</div>
<div class="panel-body">
<div id="current-image">
<div ng-show="currentImage.comment">
<blockquote style="margin-top: 10px;">
<span class="markdown-view" content="currentImage.comment"></span>
</blockquote>
</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>
<dl class="dl-normal">
<dt>Created</dt>
<dd am-time-ago="parseDate(currentImage.created)"></dd>
<dt>Image ID</dt>
<dd><a href="{{'/repository/' + repo.namespace + '/' + repo.name + '/image/' + currentImage.id}}">{{ currentImage.id }}</a></dd>
</dl>
<!-- Tree View itself -->
<div id="image-history-container" onresize="tree.notifyResized()"></div>
</div>
</div>
<!-- Image changes loading -->
<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">
<span class="change-count added" ng-show="currentImageChanges.added.length > 0" title="Files Added"
bs-tooltip="tooltip.title" data-placement="top">
<i class="fa fa-plus-square"></i>
<b>{{currentImageChanges.added.length}}</b>
</span>
<span class="change-count removed" ng-show="currentImageChanges.removed.length > 0" title="Files Removed"
bs-tooltip="tooltip.title" data-placement="top">
<i class="fa fa-minus-square"></i>
<b>{{currentImageChanges.removed.length}}</b>
</span>
<span class="change-count changed" ng-show="currentImageChanges.changed.length > 0" title="Files Changed"
bs-tooltip="tooltip.title" data-placement="top">
<i class="fa fa-pencil-square"></i>
<b>{{currentImageChanges.changed.length}}</b>
</span>
</div>
<!-- Side Panel -->
<div class="col-md-4">
<div class="panel panel-default">
<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>
<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">
<a href="javascript:void(0)" ng-click="setImage(image)">{{image.id.substr(0, 12)}}</a>
</li>
</ul>
<div id="collapseChanges" class="panel-collapse collapse in">
<div class="well well-sm">
<div class="change added" ng-repeat="file in currentImageChanges.added | limitTo:5">
<i class="fa fa-plus-square"></i>
<span title="{{file}}">{{file}}</span>
</div>
<div class="change removed" ng-repeat="file in currentImageChanges.removed | limitTo:5">
<i class="fa fa-minus-square"></i>
<span title="{{file}}">{{file}}</span>
</div>
<div class="change changed" ng-repeat="file in currentImageChanges.changed | limitTo:5">
<i class="fa fa-pencil-square"></i>
<span title="{{file}}">{{file}}</span>
</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>
</div>
</div>
</div>
</div>
</div>
</div>
<span class="right-title">Image</span>
</div>
<div class="panel-body">
<div id="current-image">
<div ng-show="currentImage.comment">
<blockquote style="margin-top: 10px;">
<span class="markdown-view" content="currentImage.comment"></span>
</blockquote>
</div>
<dl class="dl-normal">
<dt>Created</dt>
<dd am-time-ago="parseDate(currentImage.created)"></dd>
<dt>Image ID</dt>
<dd><a href="{{'/repository/' + repo.namespace + '/' + repo.name + '/image/' + currentImage.id}}">{{ currentImage.id }}</a></dd>
</dl>
<!-- Image changes loading -->
<div ng-hide="currentImageChanges">
<i class="fa fa-spinner fa-spin fa-3x"></i>
</div>
<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">
<span class="change-count added" ng-show="currentImageChanges.added.length > 0" title="Files Added"
bs-tooltip="tooltip.title" data-placement="top">
<i class="fa fa-plus-square"></i>
<b>{{currentImageChanges.added.length}}</b>
</span>
<span class="change-count removed" ng-show="currentImageChanges.removed.length > 0" title="Files Removed"
bs-tooltip="tooltip.title" data-placement="top">
<i class="fa fa-minus-square"></i>
<b>{{currentImageChanges.removed.length}}</b>
</span>
<span class="change-count changed" ng-show="currentImageChanges.changed.length > 0" title="Files Changed"
bs-tooltip="tooltip.title" data-placement="top">
<i class="fa fa-pencil-square"></i>
<b>{{currentImageChanges.changed.length}}</b>
</span>
</div>
<div id="collapseChanges" class="panel-collapse collapse in">
<div class="well well-sm">
<div class="change added" ng-repeat="file in currentImageChanges.added | limitTo:5">
<i class="fa fa-plus-square"></i>
<span title="{{file}}">{{file}}</span>
</div>
<div class="change removed" ng-repeat="file in currentImageChanges.removed | limitTo:5">
<i class="fa fa-minus-square"></i>
<span title="{{file}}">{{file}}</span>
</div>
<div class="change changed" ng-repeat="file in currentImageChanges.changed | limitTo:5">
<i class="fa fa-pencil-square"></i>
<span title="{{file}}">{{file}}</span>
</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>
</div>
</div>
</div>
</div>
</div>
</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>