Merge remote-tracking branch 'origin/heyyouthere'

This commit is contained in:
jakedt 2014-03-17 22:33:27 -04:00
commit eef17ae7d4
26 changed files with 4048 additions and 126 deletions

View file

@ -271,9 +271,22 @@ class LogEntry(BaseModel):
metadata_json = TextField(default='{}')
class NotificationKind(BaseModel):
name = CharField(index=True)
class Notification(BaseModel):
uuid = CharField(default=uuid_generator, index=True)
kind = ForeignKeyField(NotificationKind, index=True)
target = ForeignKeyField(User, index=True)
metadata_json = TextField(default='{}')
created = DateTimeField(default=datetime.now, index=True)
all_models = [User, Repository, Image, AccessToken, Role,
RepositoryPermission, Visibility, RepositoryTag,
EmailConfirmation, FederatedLogin, LoginService, QueueItem,
RepositoryBuild, Team, TeamMember, TeamRole, Webhook,
LogEntryKind, LogEntry, PermissionPrototype, ImageStorage,
BuildTriggerService, RepositoryBuildTrigger]
BuildTriggerService, RepositoryBuildTrigger, NotificationKind,
Notification]

View file

@ -59,7 +59,7 @@ class InvalidBuildTriggerException(DataModelException):
pass
def create_user(username, password, email):
def create_user(username, password, email, is_organization=False):
if not validate_email(email):
raise InvalidEmailAddressException('Invalid email address: %s' % email)
if not validate_username(username):
@ -93,6 +93,12 @@ def create_user(username, password, email):
new_user = User.create(username=username, password_hash=pw_hash,
email=email)
# If the password is None, then add a notification for the user to change
# their password ASAP.
if not pw_hash and not is_organization:
create_notification('password_required', new_user)
return new_user
except Exception as ex:
raise DataModelException(ex.message)
@ -101,7 +107,7 @@ def create_user(username, password, email):
def create_organization(name, email, creating_user):
try:
# Create the org
new_org = create_user(name, None, email)
new_org = create_user(name, None, email, is_organization=True)
new_org.organization = True
new_org.save()
@ -662,6 +668,9 @@ def change_password(user, new_password):
user.password_hash = pw_hash
user.save()
# Remove any password required notifications for the user.
delete_notifications_by_kind(user, 'password_required')
def change_invoice_email(user, invoice_email):
user.invoice_email = invoice_email
@ -1535,3 +1544,46 @@ def list_trigger_builds(namespace_name, repository_name, trigger_uuid,
limit):
return (list_repository_builds(namespace_name, repository_name, limit)
.where(RepositoryBuildTrigger.uuid == trigger_uuid))
def create_notification(kind, target, metadata={}):
kind_ref = NotificationKind.get(name=kind)
notification = Notification.create(kind=kind_ref, target=target,
metadata_json=json.dumps(metadata))
return notification
def list_notifications(user, kind=None):
Org = User.alias()
AdminTeam = Team.alias()
AdminTeamMember = TeamMember.alias()
AdminUser = User.alias()
query = (Notification.select()
.join(User)
.switch(Notification)
.join(Org, JOIN_LEFT_OUTER, on=(Org.id == Notification.target))
.join(AdminTeam, JOIN_LEFT_OUTER, on=(Org.id ==
AdminTeam.organization))
.join(TeamRole, JOIN_LEFT_OUTER, on=(AdminTeam.role == TeamRole.id))
.switch(AdminTeam)
.join(AdminTeamMember, JOIN_LEFT_OUTER, on=(AdminTeam.id ==
AdminTeamMember.team))
.join(AdminUser, JOIN_LEFT_OUTER, on=(AdminTeamMember.user ==
AdminUser.id)))
where_clause = ((Notification.target == user) |
((AdminUser.id == user) &
(TeamRole.name == 'admin')))
if kind:
where_clause = where_clause & (NotificationKind.name == kind)
return query.where(where_clause).order_by(Notification.created).desc()
def delete_notifications_by_kind(target, kind):
kind_ref = NotificationKind.get(name=kind)
Notification.delete().where(Notification.target == target,
Notification.kind == kind_ref).execute()

View file

@ -28,7 +28,7 @@ from auth.permissions import (ReadRepositoryPermission,
ViewTeamPermission,
UserPermission)
from endpoints.common import (common_login, get_route_data, truthy_param,
start_build)
start_build, check_repository_usage)
from endpoints.trigger import (BuildTrigger, TriggerActivationException,
TriggerDeactivationException,
EmptyRepositoryException)
@ -2197,6 +2197,7 @@ def subscribe(user, plan, token, require_business_plan):
cus = stripe.Customer.create(email=user.email, plan=plan, card=card)
user.stripe_id = cus.id
user.save()
check_repository_usage(user, plan_found)
log_action('account_change_plan', user.username, {'plan': plan})
except stripe.CardError as e:
return carderror_response(e)
@ -2213,6 +2214,7 @@ def subscribe(user, plan, token, require_business_plan):
# We only have to cancel the subscription if they actually have one
cus.cancel_subscription()
cus.save()
check_repository_usage(user, plan_found)
log_action('account_change_plan', user.username, {'plan': plan})
else:
@ -2228,6 +2230,7 @@ def subscribe(user, plan, token, require_business_plan):
return carderror_response(e)
response_json = subscription_view(cus.subscription, private_repos)
check_repository_usage(user, plan_found)
log_action('account_change_plan', user.username, {'plan': plan})
resp = jsonify(response_json)
@ -2518,3 +2521,20 @@ def get_logs(namespace, start_time, end_time, performer_name=None,
'logs': [log_view(log) for log in logs]
})
def notification_view(notification):
return {
'organization': notification.target.username if notification.target.organization else None,
'kind': notification.kind.name,
'created': notification.created,
'metadata': json.loads(notification.metadata_json),
}
@api.route('/user/notifications', methods=['GET'])
@api_login_required
def list_user_notifications():
notifications = model.list_notifications(current_user.db_user())
return jsonify({
'notifications': [notification_view(notification) for notification in notifications]
})

View file

@ -5,7 +5,7 @@ import urlparse
import json
from flask import session, make_response, render_template, request
from flask.ext.login import login_user, UserMixin
from flask.ext.login import login_user, UserMixin, current_user
from flask.ext.principal import identity_changed
from data import model
@ -120,13 +120,22 @@ app.jinja_env.globals['csrf_token'] = generate_csrf_token
def render_page_template(name, **kwargs):
resp = make_response(render_template(name, route_data=get_route_data(),
**kwargs))
resp.headers['X-FRAME-OPTIONS'] = 'DENY'
return resp
def check_repository_usage(user_or_org, plan_found):
private_repos = model.get_private_repo_count(user_or_org.username)
repos_allowed = plan_found['privateRepos']
if private_repos > repos_allowed:
model.create_notification('over_private_usage', user_or_org, {'namespace': user_or_org.username})
else:
model.delete_notifications_by_kind(user_or_org, 'over_private_usage')
def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
trigger=None):
host = urlparse.urlparse(request.url).netloc

View file

@ -203,7 +203,7 @@ class GithubBuildTrigger(BuildTrigger):
try:
repo = gh_client.get_repo(source)
default_commit = repo.get_branch(repo.master_branch).commit
default_commit = repo.get_branch(repo.master_branch or 'master').commit
commit_tree = repo.get_git_tree(default_commit.sha, recursive=True)
return [os.path.dirname(elem.path) for elem in commit_tree.tree
@ -283,4 +283,4 @@ class GithubBuildTrigger(BuildTrigger):
short_sha = GithubBuildTrigger.get_display_name(master_sha)
ref = 'refs/heads/%s' % repo.master_branch
return self._prepare_build(config, repo, master_sha, short_sha, ref)
return self._prepare_build(config, repo, master_sha, short_sha, ref)

View file

@ -224,6 +224,11 @@ def initialize_database():
LogEntryKind.create(name='setup_repo_trigger')
LogEntryKind.create(name='delete_repo_trigger')
NotificationKind.create(name='password_required')
NotificationKind.create(name='over_private_usage')
NotificationKind.create(name='test_notification')
def wipe_database():
logger.debug('Wiping all data from the DB.')
@ -261,6 +266,9 @@ def populate_database():
new_user_4.verified = True
new_user_4.save()
new_user_5 = model.create_user('unverified', 'password', 'no5@thanks.com')
new_user_5.save()
reader = model.create_user('reader', 'password', 'no1@thanks.com')
reader.verified = True
reader.save()
@ -269,6 +277,8 @@ def populate_database():
outside_org.verified = True
outside_org.save()
model.create_notification('test_notification', new_user_1, metadata={'some': 'value'})
__generate_repository(new_user_4, 'randomrepo', 'Random repo repository.', False,
[], (4, [], ['latest', 'prod']))

View file

@ -9,6 +9,57 @@
}
}
.notification-view-element {
cursor: pointer;
margin-bottom: 10px;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
position: relative;
max-width: 320px;
}
.notification-view-element .orginfo {
margin-top: 8px;
float: left;
}
.notification-view-element .orginfo .orgname {
font-size: 12px;
color: #aaa;
}
.notification-view-element .circle {
position: absolute;
top: 14px;
left: 0px;
width: 12px;
height: 12px;
display: inline-block;
border-radius: 50%;
}
.notification-view-element .datetime {
margin-top: 16px;
font-size: 12px;
color: #aaa;
text-align: right;
}
.notification-view-element .message {
margin-bottom: 4px;
}
.notification-view-element .container {
padding: 10px;
border-radius: 6px;
margin-left: 16px;
}
.notification-view-element .container:hover {
background: rgba(66, 139, 202, 0.1);
}
.dockerfile-path {
margin-top: 10px;
padding: 20px;
@ -507,7 +558,22 @@ i.toggle-icon:hover {
min-width: 200px;
}
.user-notification {
.notification-primary {
background: #428bca;
color: white;
}
.notification-info {
color: black;
background: #d9edf7;
}
.notification-warning {
color: #8a6d3b;
background: #fcf8e3;
}
.notification-error {
background: red;
}
@ -2131,16 +2197,16 @@ p.editable:hover i {
padding-right: 6px;
}
.delete-ui {
.delete-ui-element {
outline: none;
}
.delete-ui i {
.delete-ui-element i {
cursor: pointer;
vertical-align: middle;
}
.delete-ui .delete-ui-button {
.delete-ui-element .delete-ui-button {
display: inline-block;
vertical-align: middle;
color: white;
@ -2156,15 +2222,15 @@ p.editable:hover i {
transition: width 500ms ease-in-out;
}
.delete-ui .delete-ui-button button {
.delete-ui-element .delete-ui-button button {
padding: 4px;
}
.delete-ui:focus i {
.delete-ui-element:focus i {
visibility: hidden;
}
.delete-ui:focus .delete-ui-button {
.delete-ui-element:focus .delete-ui-button {
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">
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=32&d=identicon" />
{{ user.username }}
<span class="badge user-notification notification-animated" ng-show="user.askForPassword || overPlan"
bs-tooltip="(user.askForPassword ? 'A password is needed for this account<br>' : '') + (overPlan ? 'You are using more private repositories than your plan allows' : '')"
<span class="badge user-notification notification-animated"
ng-show="notificationService.notifications.length"
ng-class="notificationService.notificationClasses"
bs-tooltip=""
title="User Notifications"
data-placement="left"
data-container="body">
{{ (user.askForPassword ? 1 : 0) + (overPlan ? 1 : 0) }}
{{ notificationService.notifications.length }}
</span>
<b class="caret"></b>
</a>
@ -51,8 +54,16 @@
<li>
<a href="/user/" target="{{ appLinkTarget() }}">
Account Settings
<span class="badge user-notification" ng-show="user.askForPassword || overPlan">
{{ (user.askForPassword ? 1 : 0) + (overPlan ? 1 : 0) }}
</a>
</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>
</a>
</li>

View file

@ -0,0 +1,15 @@
<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">Notifications</h4>
</div>
<div class="aside-body">
<div ng-repeat="notification in notificationService.notifications">
<div class="notification-view" notification="notification" parent="this"></div>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,11 @@
<div class="notification-view-element">
<div class="container" ng-click="showNotification();">
<div class="circle" ng-class="getClass(notification)"></div>
<div class="message" ng-bind-html="getMessage(notification)"></div>
<div class="orginfo" ng-if="notification.organization">
<img src="//www.gravatar.com/avatar/{{ getGravatar(notification.organization) }}?s=24&d=identicon" />
<span class="orgname">{{ notification.organization }}</span>
</div>
<div class="datetime">{{ parseDate(notification.created) | date:'medium'}}</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'"
data-placement="bottom" ng-click="popupShown()">
<button class="btn btn-success" data-trigger="click"
data-content-template="static/directives/popup-input-dialog.html"
data-placement="bottom" ng-click="popupShown()" bs-popover>
<span ng-transclude></span>
</button>

View file

@ -1,4 +1,4 @@
<form name="popupinput" ng-submit="inputSubmit(); hide()" novalidate>
<input id="input-box" type="text form-control" placeholder="{{ placeholder }}" ng-blur="hide()"
<form name="popupinput" ng-submit="inputSubmit(); $hide()" novalidate>
<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>
</form>

View file

@ -48,10 +48,7 @@
<span class="role-group" current-role="prototype.role" role-changed="setRole(role, prototype)" roles="roles"></span>
</td>
<td>
<span class="delete-ui" tabindex="0">
<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>
<span class="delete-ui" delete-title="'Delete Permission'" perform-delete="deletePrototype(prototype)"></span>
</td>
</tr>
</table>

View file

@ -24,10 +24,7 @@
</a>
</td>
<td>
<span class="delete-ui" tabindex="0">
<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>
<span class="delete-ui" delete-title="'Delete Robot Account'" perform-delete="deleteRobot(robotInfo)"></span>
</td>
</tr>
</table>

View file

@ -102,10 +102,9 @@ function getMarkedDown(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;
$provide.factory('UtilService', ['$sanitize', function($sanitize) {
var utilService = {};
@ -143,6 +142,49 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
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) {
var metadataService = {};
@ -340,7 +382,6 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
anonymous: true,
username: null,
email: null,
askForPassword: false,
organizations: [],
logins: []
}
@ -438,6 +479,101 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
return userService;
}]);
$provide.factory('NotificationService', ['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', 'PlanService', 'UserService',
function($rootScope, $interval, UserService, ApiService, StringBuilderService, PlanService, UserService) {
var notificationService = {
'user': null,
'notifications': [],
'notificationClasses': [],
'notificationSummaries': []
};
var pollTimerHandle = null;
var notificationKinds = {
'test_notification': {
'level': 'primary',
'message': 'This notification is a long message for testing',
'page': '/about/'
},
'password_required': {
'level': 'error',
'message': 'In order to begin pushing and pulling repositories to Quay.io, a password must be set for your account',
'page': '/user?tab=password'
},
'over_private_usage': {
'level': 'error',
'message': 'Namespace {namespace} is over its allowed private repository count. ' +
'<br><br>Please upgrade your plan to avoid disruptions in service.',
'page': function(metadata) {
var organization = UserService.getOrganization(metadata['namespace']);
if (organization) {
return '/organization/' + metadata['namespace'] + '/admin';
} else {
return '/user';
}
}
}
};
notificationService.getPage = function(notification) {
var page = notificationKinds[notification['kind']]['page'];
if (typeof page != 'string') {
page = page(notification['metadata']);
}
return page;
};
notificationService.getMessage = function(notification) {
var kindInfo = notificationKinds[notification['kind']];
return StringBuilderService.buildString(kindInfo['message'], notification['metadata']);
};
notificationService.getClass = function(notification) {
return 'notification-' + notificationKinds[notification['kind']]['level'];
};
notificationService.getClasses = function(notifications) {
var classes = [];
for (var i = 0; i < notifications.length; ++i) {
var notification = notifications[i];
classes.push(notificationService.getClass(notification));
}
return classes.join(' ');
};
notificationService.update = function() {
var user = UserService.currentUser();
if (!user || user.anonymous) {
return;
}
ApiService.listUserNotifications().then(function(resp) {
notificationService.notifications = resp['notifications'];
notificationService.notificationClasses = notificationService.getClasses(notificationService.notifications);
});
};
notificationService.reset = function() {
$interval.cancel(pollTimerHandle);
pollTimerHandle = $interval(notificationService.update, 5 * 60 * 1000 /* five minutes */);
};
// Watch for plan changes and update.
PlanService.registerListener(this, function(plan) {
notificationService.reset();
notificationService.update();
});
// 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) {
var keyService = {}
@ -1332,7 +1468,7 @@ quayApp.directive('logsView', function () {
'repository': '=repository',
'performer': '=performer'
},
controller: function($scope, $element, $sce, Restangular, ApiService, TriggerDescriptionBuilder) {
controller: function($scope, $element, $sce, Restangular, ApiService, TriggerDescriptionBuilder, StringBuilderService) {
$scope.loading = true;
$scope.logs = null;
$scope.kindsAllowed = null;
@ -1547,43 +1683,9 @@ quayApp.directive('logsView', function () {
return $scope.chart.getColor(kind);
};
$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'
};
$scope.getDescription = function(log) {
log.metadata['_ip'] = log.ip ? log.ip : null;
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>'));
return StringBuilderService.buildString(logDescriptions[log.kind] || log.kind, log.metadata);
};
$scope.$watch('organization', update);
@ -1845,6 +1947,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 () {
var directiveDefinitionObject = {
priority: 0,
@ -1863,7 +1990,7 @@ quayApp.directive('popupInputButton', function () {
var box = $('#input-box');
box[0].value = '';
box.focus();
}, 10);
}, 40);
};
$scope.getRegexp = function(pattern) {
@ -2077,26 +2204,12 @@ quayApp.directive('headerBar', function () {
restrict: 'C',
scope: {
},
controller: function($scope, $element, $location, UserService, PlanService, ApiService) {
$scope.overPlan = false;
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.
UserService.updateUserIn($scope, checkOverPlan);
controller: function($scope, $element, $location, UserService, PlanService, ApiService, NotificationService) {
$scope.notificationService = NotificationService;
// Monitor any user changes and place the current user into the scope.
UserService.updateUserIn($scope);
// Monitor any plan changes.
PlanService.registerListener(this, checkOverPlan);
$scope.signout = function() {
ApiService.logout().then(function() {
UserService.load();
@ -3238,6 +3351,54 @@ quayApp.directive('buildProgress', function () {
});
quayApp.directive('notificationView', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/notification-view.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'notification': '=notification',
'parent': '=parent'
},
controller: function($scope, $element, $location, UserService, NotificationService) {
$scope.getMessage = function(notification) {
return NotificationService.getMessage(notification);
};
$scope.getGravatar = function(orgname) {
var organization = UserService.getOrganization(orgname);
return organization['gravatar'] || '';
};
$scope.parseDate = function(dateString) {
return Date.parse(dateString);
};
$scope.showNotification = function() {
var url = NotificationService.getPage($scope.notification);
if (url) {
var parts = url.split('?')
$location.path(parts[0]);
if (parts.length > 1) {
$location.search(parts[1]);
}
$scope.parent.$hide();
}
};
$scope.getClass = function(notification) {
return NotificationService.getClass(notification);
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('dockerfileBuildDialog', function () {
var directiveDefinitionObject = {
priority: 0,

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>
</td>
<td>
<span class="delete-ui" tabindex="0">
<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>
<span class="delete-ui" delete-title="'Delete Permission'" perform-delete="deleteRole(name, 'team')"></span>
</td>
</tr>
@ -132,10 +129,7 @@
</div>
</td>
<td>
<span class="delete-ui" tabindex="0" title="Delete Permission">
<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>
<span class="delete-ui" delete-title="'Delete Permission'" perform-delete="deleteRole(name, 'user')"></span>
</td>
</tr>
@ -180,10 +174,7 @@
</div>
</td>
<td>
<span class="delete-ui" tabindex="0">
<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>
<span class="delete-ui" delete-title="'Delete Token'" perform-delete="deleteToken(token.code)"></span>
</td>
</tr>
@ -222,10 +213,7 @@
<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>
<span class="delete-ui" delete-title="'Delete Webhook'" perform-delete="deleteWebhook(webhook)"></span>
</td>
</tr>
</tbody>

View file

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

View file

@ -16,7 +16,8 @@
<div class="repo-controls">
<!-- Builds -->
<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">
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-tasks fa-lg"></i>
@ -50,7 +51,7 @@
<!-- Admin -->
<a id="admin-cog" href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/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>
<!-- Pull Command -->
@ -170,7 +171,7 @@
<div class="tag-image-size" ng-repeat="image in getImagesForTagBySize(currentTag) | limitTo: 10">
<span class="size-limiter">
<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 class="size-title"><a href="javascript:void(0)" ng-click="setImage(image.id, true)">{{ image.id.substr(0, 12) }}</a></span>
</div>
@ -204,7 +205,8 @@
<dt ng-show="currentImage.command && currentImage.command.length">Command</dt>
<dd ng-show="currentImage.command && currentImage.command.length" class="codetooltipcontainer">
<pre class="formatted-command trimmed"
bs-tooltip="getTooltipCommand(currentImage)"
data-html="true"
bs-tooltip="" title="{{ getTooltipCommand(currentImage) }}"
data-placement="top">{{ getFormattedCommand(currentImage) }}</pre>
</dd>
</dl>
@ -294,7 +296,8 @@
<!--<i class="fa fa-archive"></i>-->
<span class="image-listing-circle"></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) }}
</span>
</div>

View file

@ -18,6 +18,8 @@
<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/lib/angular-motion.min.css">
<link rel="stylesheet" href="/static/lib/bootstrap-additions.min.css">
<!-- Icons -->
<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="//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.1/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.1/angular-animate.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.9/angular-route.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.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>
<!-- ,typeahead.js@0.10.1 -->
<script src="static/lib/loading-bar.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-mixpanel.js"></script>
<script src="static/lib/angulartics-google-analytics.js"></script>

Binary file not shown.