/** * Service which defines the supported kinds of application notifications (those items that appear * in the sidebar) and provides helper methods for working with them. */ angular.module('quay').factory('NotificationService', ['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', 'PlanService', 'CookieService', 'Features', 'Config', '$location', 'VulnerabilityService', 'UtilService', function($rootScope, $interval, UserService, ApiService, StringBuilderService, PlanService, CookieService, Features, Config, $location, VulnerabilityService, UtilService) { var notificationService = { 'user': null, 'notifications': [], 'notificationClasses': [], 'notificationSummaries': [], 'expiringAppTokens': [], 'additionalNotifications': false }; var pollTimerHandle = null; var notificationKinds = { 'test_notification': { 'level': 'primary', 'message': 'This notification is a long message for testing: {obj}', 'page': '/about/', 'dismissable': true }, 'org_team_invite': { 'level': 'primary', 'message': '{inviter} is inviting you to join team {team} under organization {org}', 'actions': [ { 'title': 'Join team', 'kind': 'primary', 'handler': function(notification) { window.location = '/confirminvite?code=' + notification.metadata['code']; } }, { 'title': 'Decline', 'kind': 'default', 'handler': function(notification) { ApiService.declineOrganizationTeamInvite(null, {'code': notification.metadata['code']}).then(function() { notificationService.update(); }); } } ] }, 'password_required': { 'level': 'error', 'message': 'In order to begin pushing and pulling repositories, a password must be set for your account', 'page': function(metadata) { return '/user/' + UserService.currentUser()['username'] + '?tab=settings'; } }, 'over_private_usage': { 'level': 'error', 'message': 'Namespace {namespace} is over its allowed private repository count. ' + '

Please upgrade your plan to avoid disruptions in service.', 'page': function(metadata) { var organization = UserService.getOrganization(metadata['namespace']); if (organization) { return '/organization/' + metadata['namespace'] + '?tab=billing'; } else { return '/user/' + metadata['namespace'] + '?tab=billing'; } } }, 'maintenance': { 'level': 'warning', 'message': 'We will be down for schedule maintenance from {from_date} to {to_date} ' + 'for {reason}. We are sorry about any inconvenience.', 'page': 'http://status.quay.io/' }, 'repo_push': { 'level': 'info', 'message': function(metadata) { if (metadata.updated_tags && Object.getOwnPropertyNames(metadata.updated_tags).length) { return 'Repository {repository} has been pushed with the following tags updated: {updated_tags}'; } else { return 'Repository {repository} has been pushed'; } }, 'page': function(metadata) { return '/repository/' + metadata.repository; }, 'dismissable': true }, 'repo_mirror_sync_started': { 'level': 'info', 'message': function(metadata) { if (metadata.message && Object.getOwnPropertyNames(metadata.message)) { return 'Repository Mirror started for {message}'; } else { return 'Repository Mirror started for {repository}'; } }, 'page': function(metadata) { return '/repository/' + metadata.repository; }, 'dismissable': true }, 'repo_mirror_sync_success': { 'level': 'info', 'message': function(metadata) { if (metadata.message && Object.getOwnPropertyNames(metadata.message)) { return 'Repository Mirror successful for {message}'; } else { return 'Repository Mirror successful for {repository}'; } }, 'page': function(metadata) { return '/repository/' + metadata.repository; }, 'dismissable': true }, 'repo_mirror_sync_failed': { 'level': 'info', 'message': function(metadata) { if (metadata.message && Object.getOwnPropertyNames(metadata.message)) { return 'Repository Mirror unsuccessful for {message}'; } else { return 'Repository Mirror unsuccessful for {repository}'; } }, 'page': function(metadata) { return '/repository/' + metadata.repository; }, 'dismissable': true }, 'build_queued': { 'level': 'info', 'message': 'A build has been queued for repository {repository}', 'page': function(metadata) { return '/repository/' + metadata.repository + '/build/' + metadata.build_id; }, 'dismissable': true }, 'build_start': { 'level': 'info', 'message': 'A build has been started for repository {repository}', 'page': function(metadata) { return '/repository/' + metadata.repository + '/build/' + metadata.build_id; }, 'dismissable': true }, 'build_success': { 'level': 'info', 'message': 'A build has succeeded for repository {repository}', 'page': function(metadata) { return '/repository/' + metadata.repository + '/build/' + metadata.build_id; }, 'dismissable': true }, 'build_failure': { 'level': 'error', 'message': 'A build has failed for repository {repository}', 'page': function(metadata) { return '/repository/' + metadata.repository + '/build/' + metadata.build_id; }, 'dismissable': true }, 'build_cancelled': { 'level': 'info', 'message': 'A build was cancelled for repository {repository}', 'page': function(metadata) { return '/repository/' + metadata.repository + '/build/' + metadata.build_id; }, 'dismissable': true }, 'vulnerability_found': { 'level': function(metadata) { var priority = metadata['vulnerability']['priority']; return VulnerabilityService.LEVELS[priority].level; }, 'message': 'A {vulnerability.priority} vulnerability was detected in repository {repository}', 'page': function(metadata) { return '/repository/' + metadata.repository + '?tab=tags'; }, 'dismissable': true }, 'service_key_submitted': { 'level': 'primary', 'message': 'Service key {kid} for service {service} requests approval

Key was created on {created_date}', 'actions': [ { 'title': 'Approve Key', 'kind': 'primary', 'handler': function(notification) { var params = { 'kid': notification.metadata.kid }; ApiService.approveServiceKey({}, params).then(function(resp) { notificationService.update(); window.location = '/superuser/?tab=servicekeys'; }, ApiService.errorDisplay('Could not approve service key')); } }, { 'title': 'Delete Key', 'kind': 'default', 'handler': function(notification) { var params = { 'kid': notification.metadata.kid }; ApiService.deleteServiceKey(null, params).then(function(resp) { notificationService.update(); }, ApiService.errorDisplay('Could not delete service key')); } } ], 'page': function(metadata) { return '/superuser/?tab=servicekeys'; }, } }; notificationService.dismissNotification = function(notification) { notification.dismissed = true; var params = { 'uuid': notification.id }; ApiService.updateUserNotification(notification, params).then(function(resp) { var index = $.inArray(notification, notificationService.notifications); if (index >= 0) { notificationService.notifications.splice(index, 1); } notificationService.update(); }, ApiService.errorDisplay('Could not update notification')); }; notificationService.getActions = function(notification) { var kindInfo = notificationKinds[notification['kind']]; if (!kindInfo) { return []; } return kindInfo['actions'] || []; }; notificationService.canDismiss = function(notification) { var kindInfo = notificationKinds[notification['kind']]; if (!kindInfo) { return false; } return !!kindInfo['dismissable']; }; notificationService.getPage = function(notification) { var kindInfo = notificationKinds[notification['kind']]; if (!kindInfo) { return null; } var page = kindInfo['page']; if (page != null && typeof page != 'string') { page = page(notification['metadata']); } return page || ''; }; notificationService.getMessage = function(notification) { var kindInfo = notificationKinds[notification['kind']]; if (!kindInfo) { return '(Unknown notification kind: ' + notification['kind'] + ')'; } return StringBuilderService.buildTrustedString(kindInfo['message'], notification['metadata']); }; notificationService.getBrowserNotificationMessage = function(notification) { var kindInfo = notificationKinds[notification['kind']]; if (!kindInfo) { return '(Unknown notification kind: ' + notification['kind'] + ')'; } const unsafeHtml = StringBuilderService.buildString(kindInfo['message'], notification['metadata']); return UtilService.removeHtmlTags(unsafeHtml); } notificationService.getClass = function(notification) { var kindInfo = notificationKinds[notification['kind']]; if (!kindInfo) { return 'notification-info'; } var level = kindInfo['level']; if (level != null && typeof level != 'string') { level = level(notification['metadata']); } return 'notification-' + level; }; notificationService.getClasses = function(notifications) { if (!notifications.length) { return ''; } 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.additionalNotifications = resp['additional']; notificationService.notificationClasses = notificationService.getClasses(notificationService.notifications); if (notificationService.notifications.length > 0 && CookieService.get('quay.enabledDesktopNotifications') === 'on') { notificationService.sendBrowserNotifications(); } }); if (Features.APP_SPECIFIC_TOKENS) { var params = { 'expiring': true }; ApiService.listAppTokens(null, params).then(function(resp) { notificationService.expiringAppTokens = resp['tokens']; }); } }; notificationService.sendBrowserNotifications = () => { let mostRecentTimestamp = parseInt(CookieService.get('quay.notifications.mostRecentTimestamp'), 10); if (!mostRecentTimestamp) { mostRecentTimestamp = new Date(notificationService.notifications[0].created).getTime(); } const newNotifications = notificationService.notifications .filter(obj => new Date(obj.created).getTime() > mostRecentTimestamp); if (newNotifications.length > 0) { let message = 'You have unread notifications'; if (newNotifications.length === 1) { message = notificationService.getBrowserNotificationMessage(newNotifications[0]); } new Notification(message, { // Chrome doesn't display SVGs for notifications, so we'll use a default if we don't have an enterprise logo icon: window.location.origin + Config.getEnterpriseLogo('/static/img/quay-logo.png'), image: window.location.origin + Config.getEnterpriseLogo('/static/img/quay-logo.png'), }); const newTimestamp = new Date(newNotifications[0].created).getTime(); CookieService.putPermanent('quay.notifications.mostRecentTimestamp', newTimestamp.toString()); } }; 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; }]);