Refactor notification for better display
- bring in CSS for patternfly - better style/alignment - notification self control - more than one notification at a time - persistent for form errors Change-Id: I0c173c80f5482f0813b673067b7bb297f2a610cc
This commit is contained in:
parent
f066e0c1cb
commit
c320348bc5
|
@ -1,11 +1,45 @@
|
|||
import React from 'react';
|
||||
import ClassNames from 'classnames';
|
||||
import Timer from '../utils/Timer';
|
||||
|
||||
export default class Notification extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this._notificationTimer = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
//create a timer for the notification if it's not persistent
|
||||
if (!this.props.persistent) {
|
||||
this._notificationTimer = new Timer(() => {
|
||||
this._hideNotification();
|
||||
}, 8000);
|
||||
}
|
||||
}
|
||||
|
||||
//hide the notification as long as it's not persistent
|
||||
_hideNotification() {
|
||||
if (this._notificationTimer && !this.props.persistent) {
|
||||
this._notificationTimer.clear();
|
||||
this.props.removeNotification();
|
||||
}
|
||||
}
|
||||
|
||||
// handles the mouse hovering over a notification unless it's a persistent notification
|
||||
_handleMouseEnter() {
|
||||
!this.props.persistent ? this._notificationTimer.pause() : null;
|
||||
}
|
||||
|
||||
// handles the mouse leaving the hover over a notification unless it's a persistent notification
|
||||
_handleMouseLeave() {
|
||||
!this.props.persistent ? this._notificationTimer.resume() : null;
|
||||
}
|
||||
|
||||
render() {
|
||||
let classes = ClassNames({
|
||||
'toast-pf': true,
|
||||
'alert': true,
|
||||
'pull-right': !this.props.persistent,
|
||||
'alert-danger': this.props.type === 'error',
|
||||
'alert-warning': this.props.type === 'warning',
|
||||
'alert-success': this.props.type === 'success',
|
||||
|
@ -23,32 +57,36 @@ export default class Notification extends React.Component {
|
|||
return (
|
||||
<div className={classes}
|
||||
role="alert"
|
||||
onMouseEnter={this.props.onMouseEnter}
|
||||
onMouseLeave={this.props.onMouseLeave}>
|
||||
onMouseEnter={this._handleMouseEnter.bind(this)}
|
||||
onMouseLeave={this._handleMouseLeave.bind(this)}>
|
||||
<span className={iconClass} aria-hidden="true"></span>
|
||||
{this.props.dismissable ?
|
||||
<button type="button"
|
||||
className="close"
|
||||
aria-label="Close"
|
||||
onClick={this.props.removeNotification.bind(this)}>
|
||||
onClick={this._hideNotification.bind(this)}>
|
||||
<span className="pficon pficon-close" aria-hidden="true"></span>
|
||||
</button> : false}
|
||||
<strong>{this.props.title}</strong> {this.props.message}
|
||||
<strong>{this.props.title}</strong>
|
||||
<p>{this.props.message}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Notification.propTypes = {
|
||||
dismissable: React.PropTypes.bool,
|
||||
dismissable: React.PropTypes.bool.isRequired,
|
||||
message: React.PropTypes.string.isRequired,
|
||||
onMouseEnter: React.PropTypes.func,
|
||||
onMouseLeave: React.PropTypes.func,
|
||||
persistent: React.PropTypes.bool,
|
||||
removeNotification: React.PropTypes.func,
|
||||
title: React.PropTypes.string,
|
||||
type: React.PropTypes.string
|
||||
};
|
||||
|
||||
Notification.defaultProps = {
|
||||
dismissable: false,
|
||||
title: '',
|
||||
type: 'error'
|
||||
type: 'error',
|
||||
persistent: false
|
||||
};
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ClassNames from 'classnames';
|
||||
|
||||
import BlankSlate from '../ui/BlankSlate';
|
||||
import NotificationListItem from './NotificationListItem';
|
||||
|
||||
export default class NotificationList extends React.Component {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.active && !nextProps.active) {
|
||||
this.props.notificationsViewed();
|
||||
}
|
||||
}
|
||||
|
||||
getNotificationsContent(notifications) {
|
||||
let notificationListItems = notifications.toList().map(notification => {
|
||||
return (
|
||||
<NotificationListItem
|
||||
key={notification.id}
|
||||
title={notification.title}
|
||||
message={notification.message}
|
||||
type={notification.type}
|
||||
timestamp={notification.timestamp}
|
||||
viewed={notification.viewed}
|
||||
removeNotification={this.props.removeNotification.bind(this, notification.id)}/>
|
||||
);
|
||||
});
|
||||
|
||||
if (notifications && notifications.size > 0) {
|
||||
return (
|
||||
<table className="table table-striped table-hover">
|
||||
<tbody>
|
||||
{notificationListItems.reverse()}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<BlankSlate iconClass="fa fa-bullhorn"
|
||||
title="No Notifications"
|
||||
message="There are no notifications at this time." />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let classes = ClassNames({
|
||||
'col-sm-12': true,
|
||||
'notifications-container': true,
|
||||
'collapsed': !this.props.active
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
{this.getNotificationsContent(this.props.notifications)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NotificationList.propTypes = {
|
||||
active: React.PropTypes.bool,
|
||||
notifications: ImmutablePropTypes.map.isRequired,
|
||||
notificationsViewed: React.PropTypes.func.isRequired,
|
||||
removeNotification: React.PropTypes.func.isRequired
|
||||
};
|
|
@ -1,55 +0,0 @@
|
|||
import React from 'react';
|
||||
import ClassNames from 'classnames';
|
||||
import {FormattedDate} from 'react-intl';
|
||||
|
||||
|
||||
export default class NotificationListItem extends React.Component {
|
||||
render() {
|
||||
let iconClass = ClassNames({
|
||||
'pficon': true,
|
||||
'pficon-ok': this.props.type === 'success',
|
||||
'pficon-info': this.props.type === 'info',
|
||||
'pficon-warning-triangle-o': this.props.type === 'warning',
|
||||
'pficon-error-circle-o': this.props.type === 'error' || !this.props.type
|
||||
});
|
||||
|
||||
let notificationDate = new Date();
|
||||
notificationDate.setTime(this.props.timestamp);
|
||||
|
||||
return (
|
||||
<tr className={this.props.viewed ? '' : 'unviewed'} role="alert">
|
||||
<td>
|
||||
<span className={iconClass} aria-hidden="true"/>
|
||||
<strong>{this.props.title}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<span>{this.props.message}</span>
|
||||
</td>
|
||||
<td>
|
||||
<FormattedDate value={notificationDate}
|
||||
month="numeric" day="numeric" year="numeric"
|
||||
hour="numeric" minute="numeric" second="numeric" />
|
||||
</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
className="btn btn-primary btn-xs pull-right"
|
||||
onClick={this.props.removeNotification}>
|
||||
Dismiss
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
NotificationListItem.propTypes = {
|
||||
message: React.PropTypes.string.isRequired,
|
||||
removeNotification: React.PropTypes.func.isRequired,
|
||||
timestamp: React.PropTypes.number.isRequired,
|
||||
title: React.PropTypes.string.isRequired,
|
||||
type: React.PropTypes.string.isRequired,
|
||||
viewed: React.PropTypes.bool.isRequired
|
||||
};
|
||||
NotificationListItem.defaultProps = {
|
||||
title: '',
|
||||
type: 'error'
|
||||
};
|
|
@ -1,66 +0,0 @@
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import React from 'react';
|
||||
|
||||
export default class NotificationsIndicator extends React.Component {
|
||||
|
||||
getStatusInfo() {
|
||||
// Generate an object of status counts {'status': statusCount...}
|
||||
let stateCounts = this.props.notifications.countBy(n => n.type);
|
||||
|
||||
let unreadCount = this.props.notifications.countBy(notification => {
|
||||
return notification.viewed ? 'read' : 'unread';
|
||||
}).get('unread') || 0;
|
||||
|
||||
return {
|
||||
total: this.props.notifications.size,
|
||||
unread: unreadCount,
|
||||
error: (stateCounts.error || 0) + (stateCounts.undefined || 0),
|
||||
warning: stateCounts.warning || 0
|
||||
};
|
||||
}
|
||||
|
||||
getStatusBadge (statusInfo) {
|
||||
if (statusInfo.error > 0 || statusInfo.warning > 0) {
|
||||
let badgeClasses = 'badge';
|
||||
let statusText = '';
|
||||
let statusCount = 0;
|
||||
|
||||
if (statusInfo.error > 0) {
|
||||
badgeClasses += ' error';
|
||||
statusText = 'Errors';
|
||||
statusCount = statusInfo.error;
|
||||
}
|
||||
else {
|
||||
badgeClasses += ' warning';
|
||||
statusText = 'Warnings';
|
||||
statusCount = statusInfo.warning;
|
||||
}
|
||||
return (
|
||||
<div className="pull-right">
|
||||
<span className="indicator-category">{statusText}</span>
|
||||
<span className={badgeClasses}>{statusCount}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let statusInfo = this.getStatusInfo();
|
||||
|
||||
return (
|
||||
<a onClick={this.props.onClick} className="indicator">
|
||||
<span className="indicator-title">Notifications:</span>
|
||||
<span className="indicator-category">Unread</span>
|
||||
<span className="badge unread">{statusInfo.unread}</span>
|
||||
{this.getStatusBadge(statusInfo)}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
NotificationsIndicator.propTypes = {
|
||||
notifications: ImmutablePropTypes.map.isRequired,
|
||||
onClick: React.PropTypes.func
|
||||
};
|
|
@ -3,117 +3,53 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import React from 'react';
|
||||
|
||||
import Notification from './Notification';
|
||||
import NotificationActions from '../../actions/NotificationActions';
|
||||
|
||||
export default class NotificationsToaster extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
toasterNotification: null,
|
||||
nextNotification: null,
|
||||
timestamp: Date.now(),
|
||||
isHovering: false,
|
||||
timeout: false
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this._onNotificationsChange(nextProps);
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps, nextState) {
|
||||
if (nextState.toasterNotification) {
|
||||
if (nextState.toasterNotification != this.state.toasterNotification) {
|
||||
this.startTimer();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.isHovering && !nextState.isHovering && !this.state.timeout) {
|
||||
this.showNextNotification();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.clearTimer();
|
||||
}
|
||||
|
||||
_onNotificationsChange(nextProps) {
|
||||
let now = new Date();
|
||||
|
||||
// Determine which notifications are new
|
||||
let newNotifications = nextProps.notifications
|
||||
.filter(notification => notification.timestamp > this.state.timestamp);
|
||||
if (!newNotifications.isEmpty()) {
|
||||
let nextNotification = newNotifications.first();
|
||||
|
||||
if (!this.state.toasterNotification || !this.state.isHovering) {
|
||||
this.setState({toasterNotification: nextNotification, timestamp: now.getTime()});
|
||||
}
|
||||
else {
|
||||
this.setState({nextNotification: nextNotification, timestamp: now.getTime()});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showNextNotification() {
|
||||
this.setState({
|
||||
toasterNotification: this.state.nextNotification,
|
||||
nextNotification: null
|
||||
renderNotifications(){
|
||||
return this.props.notifications.toList().map(notification => {
|
||||
return (
|
||||
<Notification
|
||||
key={notification.id}
|
||||
title={notification.title}
|
||||
message={notification.message}
|
||||
type={notification.type}
|
||||
dismissable={notification.dismissable}
|
||||
removeNotification={this.props.removeNotification.bind(this, notification.id)}/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
toasterTimeout() {
|
||||
this.setState({timeout: false});
|
||||
if (!this.state.isHovering) {
|
||||
this.showNextNotification();
|
||||
}
|
||||
}
|
||||
|
||||
startTimer() {
|
||||
// Clear any previous timers
|
||||
this.clearTimer();
|
||||
this.setState({timeout: setTimeout(this.toasterTimeout.bind(this), 8000)});
|
||||
}
|
||||
|
||||
clearTimer() {
|
||||
if (this.state.timeout) {
|
||||
clearTimeout(this.state.timeout);
|
||||
this.setState({timeout: false});
|
||||
}
|
||||
}
|
||||
|
||||
closeNotification() {
|
||||
this.setState({isHovering: false});
|
||||
this.clearTimer();
|
||||
this.showNextNotification();
|
||||
}
|
||||
|
||||
notificationHover(isHover) {
|
||||
this.setState({isHovering: isHover});
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.state.toasterNotification ? (
|
||||
<div className="notification-toaster col-lg-5 col-md-6 col-sm-8 col-xs-12">
|
||||
<Notification
|
||||
title={this.state.toasterNotification.title}
|
||||
message={this.state.toasterNotification.message}
|
||||
type={this.state.toasterNotification.type}
|
||||
dismissable
|
||||
removeNotification={this.closeNotification.bind(this)}
|
||||
onMouseEnter={this.notificationHover.bind(this, true)}
|
||||
onMouseLeave={this.notificationHover.bind(this, false)}/>
|
||||
return (
|
||||
<div className="toast-pf-max-width toast-pf-top-right">
|
||||
{this.renderNotifications()}
|
||||
</div>
|
||||
) : null;
|
||||
);
|
||||
}
|
||||
}
|
||||
NotificationsToaster.propTypes = {
|
||||
notifications: ImmutablePropTypes.map.isRequired
|
||||
notifications: ImmutablePropTypes.map.isRequired,
|
||||
removeNotification: React.PropTypes.func
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
notifications: state.notifications.get('all').sortBy(n => n.timestamp)
|
||||
//TODO: write a selector for visible notifications
|
||||
//notifications: getVisibileNotifications(state).sortBy(n => n.timestamp)
|
||||
};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
removeNotification: notificationId =>
|
||||
dispatch(NotificationActions.removeNotification(notificationId))
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(NotificationsToaster);
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(NotificationsToaster);
|
||||
|
|
|
@ -11,6 +11,7 @@ export default class FormErrorList extends React.Component {
|
|||
title={error.title}
|
||||
message={error.message}
|
||||
dismissable={false}
|
||||
persistent
|
||||
type={error.type}/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
export default function Timer (callback, delay) {
|
||||
var timerId;
|
||||
var start;
|
||||
var remaining = delay;
|
||||
|
||||
this.pause = function() {
|
||||
clearTimeout(timerId);
|
||||
remaining -= new Date() - start;
|
||||
};
|
||||
|
||||
this.resume = function() {
|
||||
start = new Date();
|
||||
clearTimeout(timerId);
|
||||
timerId = setTimeout(callback, remaining);
|
||||
};
|
||||
|
||||
this.clear = function() {
|
||||
clearTimeout(timerId);
|
||||
};
|
||||
|
||||
this.resume();
|
||||
}
|
|
@ -94,7 +94,6 @@
|
|||
@import "components/DeploymentPlan";
|
||||
@import "components/NodeStack";
|
||||
@import "components/NodePicker";
|
||||
@import "components/NotificationList";
|
||||
@import "components/Plans";
|
||||
@import "components/Validations";
|
||||
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
.notification-toaster {
|
||||
position: fixed;
|
||||
right:0;
|
||||
top: 0px;
|
||||
margin-top: 10px;
|
||||
margin-left: 10px;
|
||||
z-index: 1050;
|
||||
|
||||
&.on-login {
|
||||
top: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.notifications-container {
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
&.collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.table > tbody > tr {
|
||||
&.unviewed {
|
||||
td {
|
||||
background-color: #fbeabc;
|
||||
border-color: #f8d885;
|
||||
}
|
||||
&:hover {
|
||||
td {
|
||||
background-color: #f9d67a;
|
||||
border-color: #f5c12e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pficon {
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
vertical-align: middle;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
visibility: hidden;
|
||||
margin-right: 5px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.btn {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue