Fetch unread notifications number

Replace 20sec polling of the whole notification collection
by polling of notification statistics (GET /api/notifications/stats)
which includes number of unread notifications.
This number is rendered in notifications badge in Fuel UI.

Also, notifications are marked as read by a new handler
PUT /api/notifications/change_status with {status: 'read'} payload.

Partial-Bug: #1657348

Depends-On: I2e6a0daaf8712ab3064df728a8fb463ef805aa06

Change-Id: I6a7eae7abf2b43143039db7ca262ae40ce5a30b4
This commit is contained in:
Julia Aranovich 2017-01-30 12:53:09 +03:00
parent d5b2ee5330
commit 44fa844e61
6 changed files with 95 additions and 56 deletions

View File

@ -170,8 +170,8 @@ class App {
this.version = new models.FuelVersion();
this.fuelSettings = new models.FuelSettings();
this.user = new models.User();
this.statistics = new models.NodesStatistics();
this.notifications = new models.Notifications();
this.nodeStatistics = new models.NodeStatistics();
this.notificationStatistics = new models.NotificationStatistics();
this.releases = new models.Releases();
this.keystoneClient = new KeystoneClient('/keystone');
}
@ -231,7 +231,9 @@ class App {
var wrappedRootComponent = ReactDOM.render(
React.createElement(
RootComponent,
_.pick(this, 'version', 'user', 'fuelSettings', 'statistics', 'notifications')
_.pick(this,
'version', 'user', 'fuelSettings', 'nodeStatistics', 'notificationStatistics'
)
),
this.mountNode[0]
);

View File

@ -568,8 +568,8 @@ models.Nodes = BaseCollection.extend({
}
});
models.NodesStatistics = BaseModel.extend({
constructorName: 'NodesStatistics',
models.NodeStatistics = BaseModel.extend({
constructorName: 'NodeStatistics',
urlRoot: '/api/nodes/allocation/stats'
});
@ -757,6 +757,11 @@ models.Notifications = BaseCollection.extend({
}
});
models.NotificationStatistics = BaseModel.extend({
constructorName: 'NotificationStatistics',
urlRoot: '/api/notifications/stats'
});
models.Settings = BaseModel
.extend(deepModelMixin)
.extend(cacheMixin)

View File

@ -1062,6 +1062,9 @@ input[type=range] {
.font-semibold;
font-size: @base-font-size;
}
.progress {
margin-bottom: 0;
}
}
&.user-popover {

View File

@ -21,8 +21,9 @@ import Backbone from 'backbone';
import React from 'react';
import utils from 'utils';
import models from 'models';
import dispatcher from 'dispatcher';
import {backboneMixin, pollingMixin, dispatcherMixin} from 'component_mixins';
import {Popover, Link} from 'views/controls';
import {Popover, Link, ProgressBar} from 'views/controls';
import {ChangePasswordDialog, ShowNodeInfoDialog} from 'views/dialogs';
export var Navbar = React.createClass({
@ -31,8 +32,8 @@ export var Navbar = React.createClass({
dispatcherMixin('updateNotifications', 'updateNotifications'),
backboneMixin('user'),
backboneMixin('version'),
backboneMixin('statistics'),
backboneMixin('notifications', 'update change:status'),
backboneMixin('nodeStatistics'),
backboneMixin('notificationStatistics'),
pollingMixin(20)
],
togglePopover(popoverName) {
@ -53,15 +54,15 @@ export var Navbar = React.createClass({
},
fetchData() {
return Promise.all([
this.props.statistics.fetch(),
this.props.notifications.fetch({limit: this.props.notificationsDisplayCount})
this.props.nodeStatistics.fetch(),
this.props.notificationStatistics.fetch()
]);
},
updateNodeStats() {
return this.props.statistics.fetch();
return this.props.nodeStatistics.fetch();
},
updateNotifications() {
return this.props.notifications.fetch({limit: this.props.notificationsDisplayCount});
return this.props.notificationStatistics.fetch();
},
componentDidMount() {
this.props.user.on('change:authenticated', (model, value) => {
@ -69,14 +70,13 @@ export var Navbar = React.createClass({
this.startPolling();
} else {
this.stopPolling();
this.props.statistics.clear();
this.props.notifications.reset();
this.props.nodeStatistics.clear();
this.props.notificationStatistics.clear();
}
});
},
getDefaultProps() {
return {
notificationsDisplayCount: 5,
elements: [
{label: 'environments', url: '/clusters'},
{label: 'equipment', url: '/equipment'},
@ -87,15 +87,16 @@ export var Navbar = React.createClass({
};
},
getInitialState() {
return {};
return {notifications: new models.Notifications()};
},
scrollToTop() {
$('html, body').animate({scrollTop: 0}, 'fast');
},
render() {
var unreadNotificationsCount = this.props.notifications.filter({status: 'unread'}).length;
var authenticationEnabled = this.props.version.get('auth_required') &&
this.props.user.get('authenticated');
var {
user, version, elements, activeElement, nodeStatistics, notificationStatistics
} = this.props;
var authenticationEnabled = version.get('auth_required') && user.get('authenticated');
return (
<div className='navigation-box'>
@ -107,11 +108,11 @@ export var Navbar = React.createClass({
</div>
<div className='col-xs-6'>
<ul className='nav navbar-nav pull-left'>
{_.map(this.props.elements, (element) => {
{_.map(elements, (element) => {
return (
<li
className={utils.classNames({
active: this.props.activeElement === element.url.slice(1)
active: activeElement === element.url.slice(1)
})}
key={element.label}
>
@ -138,16 +139,16 @@ export var Navbar = React.createClass({
</li>
<li
key='statistics-icon'
className={
'statistics-icon ' +
(this.props.statistics.get('unallocated') ? '' : 'no-unallocated')
}
className={utils.classNames({
'statistics-icon': true,
'no-unallocated': !nodeStatistics.get('unallocated')
})}
onClick={this.togglePopover('statistics')}
>
{!!this.props.statistics.get('unallocated') &&
<div className='unallocated'>{this.props.statistics.get('unallocated')}</div>
{!!nodeStatistics.get('unallocated') &&
<div className='unallocated'>{nodeStatistics.get('unallocated')}</div>
}
<div className='total'>{this.props.statistics.get('total')}</div>
<div className='total'>{nodeStatistics.get('total')}</div>
</li>
{authenticationEnabled &&
<li
@ -162,9 +163,12 @@ export var Navbar = React.createClass({
onClick={this.togglePopover('notifications')}
>
<span
className={utils.classNames({badge: true, visible: unreadNotificationsCount})}
className={utils.classNames({
badge: true,
visible: !!notificationStatistics.get('unread')
})}
>
{unreadNotificationsCount}
{notificationStatistics.get('unread')}
</span>
</li>
@ -177,22 +181,22 @@ export var Navbar = React.createClass({
{this.state.statisticsPopoverVisible &&
<StatisticsPopover
key='statistics-popover'
statistics={this.props.statistics}
statistics={nodeStatistics}
toggle={this.togglePopover('statistics')}
/>
}
{this.state.userPopoverVisible &&
<UserPopover
key='user-popover'
user={this.props.user}
user={user}
toggle={this.togglePopover('user')}
/>
}
{this.state.notificationsPopoverVisible &&
<NotificationsPopover
notifications={this.state.notifications}
notificationStatistics={notificationStatistics}
key='notifications-popover'
notifications={this.props.notifications}
displayCount={this.props.notificationsDisplayCount}
toggle={this.togglePopover('notifications')}
/>
}
@ -293,7 +297,26 @@ var UserPopover = React.createClass({
});
var NotificationsPopover = React.createClass({
mixins: [backboneMixin('notifications')],
mixins: [
backboneMixin('notifications'),
backboneMixin('notificationStatistics', 'change:total')
],
getDefaultProps() {
return {visibleNotificationsNumber: 5};
},
componentWillMount() {
this.updateNotifications()
.then(() => this.setState({loading: false}));
},
componentWillReceiveProps() {
this.updateNotifications();
},
updateNotifications() {
var {notifications} = this.props;
//FIXME(jkirnosova): need to fetch limited number of notifications
//according to visibleNotificationsNumber prop
return notifications.fetch().then(() => this.markAsRead());
},
showNodeInfo(id) {
this.props.toggle(false);
var node = new models.Node({id});
@ -306,20 +329,19 @@ var NotificationsPopover = React.createClass({
);
if (notificationsToMark.length) {
this.setState({unreadNotificationsIds: notificationsToMark.map('id')});
notificationsToMark.toJSON = function() {
return notificationsToMark.map((notification) => {
notification.set({status: 'read'});
return _.pick(notification.attributes, 'id', 'status');
});
};
Backbone.sync('update', notificationsToMark);
notificationsToMark.invokeMap('set', {status: 'read'});
Backbone.sync('update', notificationsToMark, {
url: _.result(notificationsToMark, 'url') + '/change_status',
data: JSON.stringify({status: 'read'})
})
.then(() => dispatcher.trigger('updateNotifications'));
}
},
componentDidMount() {
this.markAsRead();
},
getInitialState() {
return {unreadNotificationsIds: []};
return {
loading: true,
unreadNotificationsIds: []
};
},
renderNotification(notification) {
var nodeId = notification.get('node_id');
@ -352,14 +374,21 @@ var NotificationsPopover = React.createClass({
);
},
render() {
var {loading} = this.state;
var {notifications, toggle, visibleNotificationsNumber} = this.props;
var showMore = Backbone.history.getHash() !== 'notifications';
var {notifications, displayCount, toggle} = this.props;
return (
<Popover toggle={toggle} className='notifications-popover'>
{_.map(notifications.take(displayCount), this.renderNotification)}
{showMore &&
<div className='show-more'>
<Link to='/notifications'>{i18n('notifications_popover.view_all_button')}</Link>
{loading ?
<ProgressBar />
:
<div>
{notifications.take(visibleNotificationsNumber).map(this.renderNotification)}
{showMore &&
<div className='show-more'>
<Link to='/notifications'>{i18n('notifications_popover.view_all_button')}</Link>
</div>
}
</div>
}
</Popover>

View File

@ -30,10 +30,8 @@ NotificationsPage = React.createClass({
navbarActiveElement: null,
breadcrumbsPath: [['home', '/'], 'notifications'],
fetchData() {
var notifications = app.notifications;
return notifications.fetch().then(() =>
({notifications: notifications})
);
var notifications = new models.Notifications();
return notifications.fetch().then(() => ({notifications}));
}
},
checkDateIsToday(date) {

View File

@ -75,7 +75,9 @@ var RootComponent = React.createClass({
key='navbar'
ref='navbar'
activeElement={Page.navbarActiveElement}
{... _.pick(this.props, 'version', 'user', 'statistics', 'notifications')}
{... _.pick(this.props,
'version', 'user', 'nodeStatistics', 'notificationStatistics'
)}
/>,
<Breadcrumbs
key='breadcrumbs'