- Convert Quay over to Angular 1.2.9 and the new angular-strap

- Add the beginnings of a notification service
- Add the beginnings of a notification UI
This commit is contained in:
Joseph Schorr 2014-03-12 00:49:46 -04:00
parent 368a8da7db
commit f186fa2888
18 changed files with 3794 additions and 118 deletions

View file

@ -507,7 +507,22 @@ i.toggle-icon:hover {
min-width: 200px; min-width: 200px;
} }
.user-notification { .user-notification.notification-primary {
background: #428bca;
color: white;
}
.user-notification.notification-info {
color: black;
background: #d9edf7;
}
.user-notification.notification-warning {
color: #8a6d3b;
background: #fcf8e3;
}
.user-notification.notification-error {
background: red; background: red;
} }
@ -2122,16 +2137,16 @@ p.editable:hover i {
padding-right: 6px; padding-right: 6px;
} }
.delete-ui { .delete-ui-element {
outline: none; outline: none;
} }
.delete-ui i { .delete-ui-element i {
cursor: pointer; cursor: pointer;
vertical-align: middle; vertical-align: middle;
} }
.delete-ui .delete-ui-button { .delete-ui-element .delete-ui-button {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
color: white; color: white;
@ -2147,15 +2162,15 @@ p.editable:hover i {
transition: width 500ms ease-in-out; transition: width 500ms ease-in-out;
} }
.delete-ui .delete-ui-button button { .delete-ui-element .delete-ui-button button {
padding: 4px; padding: 4px;
} }
.delete-ui:focus i { .delete-ui-element:focus i {
visibility: hidden; visibility: hidden;
} }
.delete-ui:focus .delete-ui-button { .delete-ui-element:focus .delete-ui-button {
width: 60px; width: 60px;
} }

View file

@ -0,0 +1,4 @@
<span class="delete-ui-element" ng-click="focus()">
<span class="delete-ui-button" ng-click="performDelete()"><button class="btn btn-danger">{{ buttonTitleInternal }}</button></span>
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="left" title="{{ deleteTitle }}"></i>
</span>

View file

@ -39,11 +39,14 @@
<a href="javascript:void(0)" class="dropdown-toggle user-dropdown" data-toggle="dropdown"> <a href="javascript:void(0)" class="dropdown-toggle user-dropdown" data-toggle="dropdown">
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=32&d=identicon" /> <img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=32&d=identicon" />
{{ user.username }} {{ user.username }}
<span class="badge user-notification notification-animated" ng-show="user.askForPassword || overPlan" <span class="badge user-notification notification-animated"
bs-tooltip="(user.askForPassword ? 'A password is needed for this account<br>' : '') + (overPlan ? 'You are using more private repositories than your plan allows' : '')" ng-show="notificationService.notifications.length"
ng-class="notificationService.notificationClasses"
bs-tooltip=""
title="{{ notificationService.notificationSummaries }}"
data-placement="left" data-placement="left"
data-container="body"> data-container="body">
{{ (user.askForPassword ? 1 : 0) + (overPlan ? 1 : 0) }} {{ notificationService.notifications.length }}
</span> </span>
<b class="caret"></b> <b class="caret"></b>
</a> </a>
@ -51,8 +54,16 @@
<li> <li>
<a href="/user/" target="{{ appLinkTarget() }}"> <a href="/user/" target="{{ appLinkTarget() }}">
Account Settings Account Settings
<span class="badge user-notification" ng-show="user.askForPassword || overPlan"> </a>
{{ (user.askForPassword ? 1 : 0) + (overPlan ? 1 : 0) }} </li>
<li ng-if="notificationService.notifications.length">
<a href="javascript:void(0)" data-template="/static/directives/notification-bar.html"
data-animation="am-slide-right" bs-aside="aside" data-container="body">
Notifications
<span class="badge user-notification"
ng-class="notificationService.notificationClasses"
ng-show="notificationService.notifications.length">
{{ notificationService.notifications.length }}
</span> </span>
</a> </a>
</li> </li>

View file

@ -0,0 +1,14 @@
<div class="aside" tabindex="-1" role="dialog">
<div class="aside-dialog">
<div class="aside-content">
<div class="aside-header">
<button type="button" class="close" ng-click="$hide()">&times;</button>
<h4 class="aside-title">Some title</h4>
</div>
<div class="aside-body" ng-bind="content"></div>
<div class="aside-footer">
<button type="button" class="btn btn-default" ng-click="$hide()">Close</button>
</div>
</div>
</div>
</div>

View file

@ -1,5 +1,6 @@
<button class="btn btn-success" data-trigger="click" bs-popover="'static/directives/popup-input-dialog.html'" <button class="btn btn-success" data-trigger="click"
data-placement="bottom" ng-click="popupShown()"> data-content-template="static/directives/popup-input-dialog.html"
data-placement="bottom" ng-click="popupShown()" bs-popover>
<span ng-transclude></span> <span ng-transclude></span>
</button> </button>

View file

@ -1,4 +1,4 @@
<form name="popupinput" ng-submit="inputSubmit(); hide()" novalidate> <form name="popupinput" ng-submit="inputSubmit(); $hide()" novalidate>
<input id="input-box" type="text form-control" placeholder="{{ placeholder }}" ng-blur="hide()" <input id="input-box" type="text form-control" placeholder="{{ placeholder }}" ng-blur="$hide()"
ng-pattern="getRegexp(pattern)" ng-model="inputValue" ng-trim="false" ng-minlength="2" required> ng-pattern="getRegexp(pattern)" ng-model="inputValue" ng-trim="false" ng-minlength="2" required>
</form> </form>

View file

@ -48,10 +48,7 @@
<span class="role-group" current-role="prototype.role" role-changed="setRole(role, prototype)" roles="roles"></span> <span class="role-group" current-role="prototype.role" role-changed="setRole(role, prototype)" roles="roles"></span>
</td> </td>
<td> <td>
<span class="delete-ui" tabindex="0"> <span class="delete-ui" delete-title="'Delete Permission'" perform-delete="deletePrototype(prototype)"></span>
<span class="delete-ui-button" ng-click="deletePrototype(prototype)"><button class="btn btn-danger">Delete</button></span>
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="right" title="Delete Permission"></i>
</span>
</td> </td>
</tr> </tr>
</table> </table>

View file

@ -24,10 +24,7 @@
</a> </a>
</td> </td>
<td> <td>
<span class="delete-ui" tabindex="0"> <span class="delete-ui" delete-title="'Delete Robot Account'" perform-delete="deleteRobot(robotInfo)"></span>
<span class="delete-ui-button" ng-click="deleteRobot(robotInfo)"><button class="btn btn-danger">Delete</button></span>
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="right" title="Delete Robot Account"></i>
</span>
</td> </td>
</tr> </tr>
</table> </table>

View file

@ -102,10 +102,9 @@ function getMarkedDown(string) {
return Markdown.getSanitizingConverter().makeHtml(string || ''); return Markdown.getSanitizingConverter().makeHtml(string || '');
} }
quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', '$strap.directives', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml', 'ngAnimate'], function($provide, cfpLoadingBarProvider) { quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', 'mgcrea.ngStrap', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml', 'ngAnimate'], function($provide, cfpLoadingBarProvider) {
cfpLoadingBarProvider.includeSpinner = false; cfpLoadingBarProvider.includeSpinner = false;
$provide.factory('UtilService', ['$sanitize', function($sanitize) { $provide.factory('UtilService', ['$sanitize', function($sanitize) {
var utilService = {}; var utilService = {};
@ -143,6 +142,49 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
return builderService; return builderService;
}]); }]);
$provide.factory('StringBuilderService', ['$sce', function($sce) {
var stringBuilderService = {};
stringBuilderService.buildString = function(value_or_func, metadata) {
var fieldIcons = {
'username': 'user',
'activating_username': 'user',
'delegate_user': 'user',
'delegate_team': 'group',
'team': 'group',
'token': 'key',
'repo': 'hdd-o',
'robot': 'wrench',
'tag': 'tag',
'role': 'th-large',
'original_role': 'th-large'
};
var description = value_or_func;
if (typeof description != 'string') {
description = description(metadata);
}
for (var key in metadata) {
if (metadata.hasOwnProperty(key)) {
var value = metadata[key] != null ? metadata[key].toString() : '(Unknown)';
var markedDown = getMarkedDown(value);
markedDown = markedDown.substr('<p>'.length, markedDown.length - '<p></p>'.length);
var icon = fieldIcons[key];
if (icon) {
markedDown = '<i class="fa fa-' + icon + '"></i>' + markedDown;
}
description = description.replace('{' + key + '}', '<code>' + markedDown + '</code>');
}
}
return $sce.trustAsHtml(description.replace('\n', '<br>'));
};
return stringBuilderService;
}]);
$provide.factory('ImageMetadataService', ['UtilService', function(UtilService) { $provide.factory('ImageMetadataService', ['UtilService', function(UtilService) {
var metadataService = {}; var metadataService = {};
@ -340,7 +382,6 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
anonymous: true, anonymous: true,
username: null, username: null,
email: null, email: null,
askForPassword: false,
organizations: [], organizations: [],
logins: [] logins: []
} }
@ -438,6 +479,70 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
return userService; return userService;
}]); }]);
$provide.factory('NotificationService', ['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService',
function($rootScope, $interval, UserService, ApiService, StringBuilderService) {
var notificationService = {
'user': null,
'notifications': [],
'notificationClasses': [],
'notificationSummaries': []
};
var pollTimerHandle = null;
var notificationKinds = {
'test_notification': {
'level': 'primary',
'summary': 'This is a test notification',
'message': 'This notification is a long message for testing'
}
};
notificationService.getSummary = function(notification) {
var kindInfo = notificationKinds[notification['kind']];
return StringBuilderService.buildString(kindInfo['summary'], notification['metadata']);
};
notificationService.getSummaries = function(notifications) {
var summaries = [];
for (var i = 0; i < notifications.length; ++i) {
var notification = notifications[i];
summaries.push(notificationService.getSummary(notification));
}
return summaries.join('<br>');
};
notificationService.getClasses = function(notifications) {
var classes = [];
for (var i = 0; i < notifications.length; ++i) {
var notification = notifications[i];
classes.push('notification-' + notificationKinds[notification['kind']]['level']);
}
return classes.join(' ');
};
notificationService.update = function() {
ApiService.listUserNotifications().then(function(resp) {
notificationService.notifications = resp['notifications'];
notificationService.notificationClasses = notificationService.getClasses(notificationService.notifications);
notificationService.notificationSummaries = notificationService.getSummaries(notificationService.notifications);
});
};
notificationService.reset = function() {
$interval.cancel(pollTimerHandle);
pollTimerHandle = $interval(notificationService.update, 5 * 60 * 1000 /* five minutes */);
};
// Watch for user changes and update.
$rootScope.$watch(function() { return UserService.currentUser(); }, function(currentUser) {
notificationService.reset();
notificationService.update();
});
return notificationService;
}]);
$provide.factory('KeyService', ['$location', function($location) { $provide.factory('KeyService', ['$location', function($location) {
var keyService = {} var keyService = {}
@ -1332,7 +1437,7 @@ quayApp.directive('logsView', function () {
'repository': '=repository', 'repository': '=repository',
'performer': '=performer' 'performer': '=performer'
}, },
controller: function($scope, $element, $sce, Restangular, ApiService, TriggerDescriptionBuilder) { controller: function($scope, $element, $sce, Restangular, ApiService, TriggerDescriptionBuilder, StringBuilderService) {
$scope.loading = true; $scope.loading = true;
$scope.logs = null; $scope.logs = null;
$scope.kindsAllowed = null; $scope.kindsAllowed = null;
@ -1548,42 +1653,8 @@ quayApp.directive('logsView', function () {
}; };
$scope.getDescription = function(log) { $scope.getDescription = function(log) {
var fieldIcons = {
'username': 'user',
'activating_username': 'user',
'delegate_user': 'user',
'delegate_team': 'group',
'team': 'group',
'token': 'key',
'repo': 'hdd-o',
'robot': 'wrench',
'tag': 'tag',
'role': 'th-large',
'original_role': 'th-large'
};
log.metadata['_ip'] = log.ip ? log.ip : null; log.metadata['_ip'] = log.ip ? log.ip : null;
return StringBuilderService.buildString(logDescriptions[log.kind] || log.kind, log.metadata);
var description = logDescriptions[log.kind] || log.kind;
if (typeof description != 'string') {
description = description(log.metadata);
}
for (var key in log.metadata) {
if (log.metadata.hasOwnProperty(key)) {
var value = log.metadata[key] != null ? log.metadata[key].toString() : '(Unknown)';
var markedDown = getMarkedDown(value);
markedDown = markedDown.substr('<p>'.length, markedDown.length - '<p></p>'.length);
var icon = fieldIcons[key];
if (icon) {
markedDown = '<i class="fa fa-' + icon + '"></i>' + markedDown;
}
description = description.replace('{' + key + '}', '<code>' + markedDown + '</code>');
}
}
return $sce.trustAsHtml(description.replace('\n', '<br>'));
}; };
$scope.$watch('organization', update); $scope.$watch('organization', update);
@ -1845,6 +1916,31 @@ quayApp.directive('prototypeManager', function () {
}); });
quayApp.directive('deleteUi', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/delete-ui.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'deleteTitle': '=deleteTitle',
'buttonTitle': '=buttonTitle',
'performDelete': '&performDelete'
},
controller: function($scope, $element) {
$scope.buttonTitleInternal = $scope.buttonTitle || 'Delete';
$element.children().attr('tabindex', 0);
$scope.focus = function() {
$element[0].firstChild.focus();
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('popupInputButton', function () { quayApp.directive('popupInputButton', function () {
var directiveDefinitionObject = { var directiveDefinitionObject = {
priority: 0, priority: 0,
@ -1863,7 +1959,7 @@ quayApp.directive('popupInputButton', function () {
var box = $('#input-box'); var box = $('#input-box');
box[0].value = ''; box[0].value = '';
box.focus(); box.focus();
}, 10); }, 40);
}; };
$scope.getRegexp = function(pattern) { $scope.getRegexp = function(pattern) {
@ -2077,25 +2173,11 @@ quayApp.directive('headerBar', function () {
restrict: 'C', restrict: 'C',
scope: { scope: {
}, },
controller: function($scope, $element, $location, UserService, PlanService, ApiService) { controller: function($scope, $element, $location, UserService, PlanService, ApiService, NotificationService) {
$scope.overPlan = false; $scope.notificationService = NotificationService;
var checkOverPlan = function() {
if ($scope.user.anonymous) {
$scope.overPlan = false;
return;
}
ApiService.getUserPrivateAllowed().then(function(resp) {
$scope.overPlan = !resp['privateAllowed'];
});
};
// Monitor any user changes and place the current user into the scope. // Monitor any user changes and place the current user into the scope.
UserService.updateUserIn($scope, checkOverPlan); UserService.updateUserIn($scope);
// Monitor any plan changes.
PlanService.registerListener(this, checkOverPlan);
$scope.signout = function() { $scope.signout = function() {
ApiService.logout().then(function() { ApiService.logout().then(function() {

8
static/lib/angular-motion.min.css vendored Normal file

File diff suppressed because one or more lines are too long

3543
static/lib/angular-strap.js vendored Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

9
static/lib/angular-strap.tpl.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -112,10 +112,7 @@
<span class="role-group" current-role="permission.role" role-changed="setRole(role, name, 'team')" roles="roles"></span> <span class="role-group" current-role="permission.role" role-changed="setRole(role, name, 'team')" roles="roles"></span>
</td> </td>
<td> <td>
<span class="delete-ui" tabindex="0"> <span class="delete-ui" delete-title="'Delete Permission'" perform-delete="deleteRole(name, 'team')"></span>
<span class="delete-ui-button" ng-click="deleteRole(name, 'team')"><button class="btn btn-danger">Delete</button></span>
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="right" title="Delete Permission"></i>
</span>
</td> </td>
</tr> </tr>
@ -132,10 +129,7 @@
</div> </div>
</td> </td>
<td> <td>
<span class="delete-ui" tabindex="0" title="Delete Permission"> <span class="delete-ui" delete-title="'Delete Permission'" perform-delete="deleteRole(name, 'user')"></span>
<span class="delete-ui-button" ng-click="deleteRole(name, 'user')"><button class="btn btn-danger">Delete</button></span>
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="right" title="Delete Permission"></i>
</span>
</td> </td>
</tr> </tr>
@ -180,10 +174,7 @@
</div> </div>
</td> </td>
<td> <td>
<span class="delete-ui" tabindex="0"> <span class="delete-ui" delete-title="'Delete Token'" perform-delete="deleteToken(token.code)"></span>
<span class="delete-ui-button" ng-click="deleteToken(token.code)"><button class="btn btn-danger" type="button">Delete</button></span>
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="right" title="Delete Token"></i>
</span>
</td> </td>
</tr> </tr>
@ -222,10 +213,7 @@
<tr ng-repeat="webhook in webhooks"> <tr ng-repeat="webhook in webhooks">
<td>{{ webhook.parameters.url }}</td> <td>{{ webhook.parameters.url }}</td>
<td> <td>
<span class="delete-ui" tabindex="0"> <span class="delete-ui" delete-title="'Delete Webhook'" perform-delete="deleteWebhook(webhook)"></span>
<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> </td>
</tr> </tr>
</tbody> </tbody>

View file

@ -17,10 +17,8 @@
<span class="entity-reference" entity="member" namespace="organization.name"></span> <span class="entity-reference" entity="member" namespace="organization.name"></span>
</td> </td>
<td> <td>
<span class="delete-ui" tabindex="0" title="Remove User" ng-show="canEditMembers"> <span class="delete-ui" delete-title="'Remove User From Team'" button-title="'Remove'"
<span class="delete-ui-button" ng-click="removeMember(member.name)"><button class="btn btn-danger">Remove</button></span> perform-delete="removeMember(member.name)" ng-if="canEditMembers"></span>
<i class="fa fa-times"></i>
</span>
</td> </td>
</tr> </tr>

View file

@ -16,7 +16,8 @@
<div class="repo-controls"> <div class="repo-controls">
<!-- Builds --> <!-- Builds -->
<div class="dropdown" data-placement="top" style="display: inline-block" <div class="dropdown" data-placement="top" style="display: inline-block"
bs-tooltip="runningBuilds.length ? 'Dockerfile Builds Running: ' + (runningBuilds.length) : 'Dockerfile Build'" bs-tooltip=""
title="{{ runningBuilds.length ? 'Dockerfile Builds Running: ' + (runningBuilds.length) : 'Dockerfile Build' }}"
ng-show="repo.can_write || buildHistory.length"> ng-show="repo.can_write || buildHistory.length">
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown"> <button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-tasks fa-lg"></i> <i class="fa fa-tasks fa-lg"></i>
@ -50,7 +51,7 @@
<!-- Admin --> <!-- Admin -->
<a id="admin-cog" href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/admin' }}" <a id="admin-cog" href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/admin' }}"
ng-show="repo.can_admin"> ng-show="repo.can_admin">
<button class="btn btn-default" title="Repository Settings" bs-tooltip="tooltip.title" data-placement="top"> <button class="btn btn-default" title="Repository Settings" bs-tooltip="tooltip" data-placement="top">
<i class="fa fa-cog fa-lg"></i></button></a> <i class="fa fa-cog fa-lg"></i></button></a>
<!-- Pull Command --> <!-- Pull Command -->
@ -170,7 +171,7 @@
<div class="tag-image-size" ng-repeat="image in getImagesForTagBySize(currentTag) | limitTo: 10"> <div class="tag-image-size" ng-repeat="image in getImagesForTagBySize(currentTag) | limitTo: 10">
<span class="size-limiter"> <span class="size-limiter">
<span class="size-bar" style="{{ 'width:' + (image.size / getTotalSize(currentTag)) * 100 + '%' }}" <span class="size-bar" style="{{ 'width:' + (image.size / getTotalSize(currentTag)) * 100 + '%' }}"
bs-tooltip="image.size | bytes"></span> bs-tooltip="" title="{{ image.size | bytes }}"></span>
</span> </span>
<span class="size-title"><a href="javascript:void(0)" ng-click="setImage(image.id, true)">{{ image.id.substr(0, 12) }}</a></span> <span class="size-title"><a href="javascript:void(0)" ng-click="setImage(image.id, true)">{{ image.id.substr(0, 12) }}</a></span>
</div> </div>
@ -204,7 +205,8 @@
<dt ng-show="currentImage.command && currentImage.command.length">Command</dt> <dt ng-show="currentImage.command && currentImage.command.length">Command</dt>
<dd ng-show="currentImage.command && currentImage.command.length" class="codetooltipcontainer"> <dd ng-show="currentImage.command && currentImage.command.length" class="codetooltipcontainer">
<pre class="formatted-command trimmed" <pre class="formatted-command trimmed"
bs-tooltip="getTooltipCommand(currentImage)" data-html="true"
bs-tooltip="" title="{{ getTooltipCommand(currentImage) }}"
data-placement="top">{{ getFormattedCommand(currentImage) }}</pre> data-placement="top">{{ getFormattedCommand(currentImage) }}</pre>
</dd> </dd>
</dl> </dl>
@ -294,7 +296,8 @@
<!--<i class="fa fa-archive"></i>--> <!--<i class="fa fa-archive"></i>-->
<span class="image-listing-circle"></span> <span class="image-listing-circle"></span>
<span class="image-listing-line"></span> <span class="image-listing-line"></span>
<span class="context-tooltip image-listing-id" bs-tooltip="getFirstTextLine(image.comment)"> <span class="context-tooltip image-listing-id" bs-tooltip="" title="{{ getFirstTextLine(image.comment) }}"
data-html="true">
{{ image.id.substr(0, 12) }} {{ image.id.substr(0, 12) }}
</span> </span>
</div> </div>

View file

@ -18,6 +18,8 @@
<link href='//fonts.googleapis.com/css?family=Droid+Sans:400,700' rel='stylesheet' type='text/css'> <link href='//fonts.googleapis.com/css?family=Droid+Sans:400,700' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="/static/css/quay.css"> <link rel="stylesheet" href="/static/css/quay.css">
<link rel="stylesheet" href="/static/lib/angular-motion.min.css">
<link rel="stylesheet" href="/static/lib/bootstrap-additions.min.css">
<!-- Icons --> <!-- Icons -->
<link rel="shortcut icon" href="/static/img/favicon.ico" type="image/x-icon" /> <link rel="shortcut icon" href="/static/img/favicon.ico" type="image/x-icon" />
@ -39,16 +41,17 @@
<script src="//code.jquery.com/jquery.js"></script> <script src="//code.jquery.com/jquery.js"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script> <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.9/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular-route.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.9/angular-route.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular-sanitize.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.9/angular-sanitize.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular-animate.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.9/angular-animate.min.js"></script>
<script src="//cdn.jsdelivr.net/g/bootbox@4.1.0,underscorejs@1.5.2,restangular@1.2.0"></script> <script src="//cdn.jsdelivr.net/g/bootbox@4.1.0,underscorejs@1.5.2,restangular@1.2.0"></script>
<!-- ,typeahead.js@0.10.1 --> <!-- ,typeahead.js@0.10.1 -->
<script src="static/lib/loading-bar.js"></script> <script src="static/lib/loading-bar.js"></script>
<script src="static/lib/angular-strap.min.js"></script> <script src="static/lib/angular-strap.min.js"></script>
<script src="static/lib/angular-strap.tpl.min.js"></script>
<script src="static/lib/angulartics.js"></script> <script src="static/lib/angulartics.js"></script>
<script src="static/lib/angulartics-mixpanel.js"></script> <script src="static/lib/angulartics-mixpanel.js"></script>
<script src="static/lib/angulartics-google-analytics.js"></script> <script src="static/lib/angulartics-google-analytics.js"></script>