Merge remote-tracking branch 'origin/heyyouthere'
This commit is contained in:
commit
eef17ae7d4
26 changed files with 4048 additions and 126 deletions
|
@ -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]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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]
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
10
initdb.py
10
initdb.py
|
@ -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']))
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
4
static/directives/delete-ui.html
Normal file
4
static/directives/delete-ui.html
Normal 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>
|
|
@ -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>
|
||||
|
|
15
static/directives/notification-bar.html
Normal file
15
static/directives/notification-bar.html
Normal 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()">×</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>
|
11
static/directives/notification-view.html
Normal file
11
static/directives/notification-view.html
Normal 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>
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
281
static/js/app.js
281
static/js/app.js
|
@ -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
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
3543
static/lib/angular-strap.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
12
static/lib/angular-strap.min.js
vendored
12
static/lib/angular-strap.min.js
vendored
File diff suppressed because one or more lines are too long
9
static/lib/angular-strap.tpl.min.js
vendored
Normal file
9
static/lib/angular-strap.tpl.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/lib/bootstrap-additions.min.css
vendored
Normal file
1
static/lib/bootstrap-additions.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
Reference in a new issue