Revision 1c40f7e8
Added by Gail Steiger over 7 years ago
app/assets/stylesheets/patternfly_and_overrides.scss | ||
---|---|---|
}
|
||
}
|
||
}
|
||
|
||
// gridster on dashboard conflicts with notifications drawer
|
||
.gridster .gs-w {
|
||
z-index: auto !important;
|
||
}
|
||
|
||
// workaround to patternfly css after removing class navbar-pf from navbar
|
||
// to retain original navbar height
|
||
.navbar-outer .drawer-pf {
|
||
height: calc(100vh - 46px);
|
||
top: 26px;
|
||
}
|
app/views/home/_topbar.html.erb | ||
---|---|---|
<%= render_menu :header_menu %>
|
||
<%= render 'home/user_dropdown' if SETTINGS[:login] %>
|
||
</ul>
|
||
<% if SETTINGS[:login] %>
|
||
<div id='notification_drawer'> </div>
|
||
<%= mount_react_component('NotificationDrawer', '#notification_drawer', {}) %>
|
||
<% end %>
|
||
</div>
|
||
<% end %>
|
||
</div>
|
app/views/home/_user_dropdown.html.erb | ||
---|---|---|
<li id='notification_icon' class="drawer-pf-trigger dropdown"> </li>
|
||
<%= mount_react_component('NotificationDrawerToggle', '#notification_icon', {:url => notification_recipients_path}.to_json ) %>
|
||
<li class="dropdown menu_tab_dropdown">
|
||
<%= user_header %>
|
||
<ul class="dropdown-menu pull-right">
|
package.json | ||
---|---|---|
"jquery.cookie": "~1.4.1",
|
||
"jstz": "~1.0.7",
|
||
"lodash": "~4.15.0",
|
||
"moment": "^2.17.1",
|
||
"multiselect": "~0.9.12",
|
||
"react": "^15.1.0",
|
||
"react-bootstrap": "^0.30.0",
|
||
... | ... | |
"automock": true,
|
||
"verbose": true,
|
||
"collectCoverage": true,
|
||
"collectCoverageFrom": ["webpack/**/*.js", "!webpack/**/bundle*"],
|
||
"coverageReporters": ["lcov"],
|
||
"collectCoverageFrom": [
|
||
"webpack/**/*.js",
|
||
"!webpack/**/bundle*"
|
||
],
|
||
"coverageReporters": [
|
||
"lcov"
|
||
],
|
||
"unmockedModulePathPatterns": [
|
||
"react",
|
||
"node_modules/"
|
webpack/assets/javascripts/react_app/API.js | ||
---|---|---|
.error((jqXHR, textStatus, errorThrown) => {
|
||
ServerActions.hostsRequestError(jqXHR, textStatus, errorThrown);
|
||
});
|
||
},
|
||
getNotifications(url) {
|
||
$.get(url)
|
||
.success(
|
||
(response, textStatus, jqXHR) => {
|
||
ServerActions.receivedNotifications(response, textStatus, jqXHR);
|
||
})
|
||
.error((jqXHR, textStatus, errorThrown) => {
|
||
ServerActions.notificationsRequestError(jqXHR, textStatus, errorThrown);
|
||
});
|
||
},
|
||
markNotificationAsRead(url) {
|
||
const data = JSON.stringify({'seen': true});
|
||
|
||
$.ajax({
|
||
url: url,
|
||
contentType: 'application/json',
|
||
type: 'put',
|
||
dataType: 'json',
|
||
data: data,
|
||
success: function (response, textstatus, jqXHR) {
|
||
ServerActions.notificationMarkedAsRead(response, textstatus, jqXHR);
|
||
},
|
||
error: function (jqXHR, textStatus, errorThrown) {
|
||
console.log(jqXHR);
|
||
}
|
||
});
|
||
}
|
||
};
|
webpack/assets/javascripts/react_app/actions/NotificationActions.js | ||
---|---|---|
import API from '../API';
|
||
import AppDispatcher from '../dispatcher';
|
||
import {ACTIONS} from '../constants';
|
||
|
||
const TIMER = 10000;
|
||
|
||
export default {
|
||
getNotifications(url) {
|
||
if (document.visibilityState === 'visible') {
|
||
API.getNotifications(url);
|
||
}
|
||
setTimeout(() => {
|
||
this.getNotifications(url);
|
||
}, TIMER);
|
||
},
|
||
toggleNotificationDrawer() {
|
||
AppDispatcher.dispatch({
|
||
actionType: ACTIONS.NOTIFICATIONS_DRAWER_TOGGLE
|
||
});
|
||
},
|
||
expandDrawerTab(group) {
|
||
AppDispatcher.dispatch({
|
||
actionType: ACTIONS.NOTIFICATIONS_EXPAND_DRAWER_TAB,
|
||
expand: group
|
||
});
|
||
},
|
||
markAsRead(url) {
|
||
API.markNotificationAsRead(url);
|
||
}
|
||
};
|
webpack/assets/javascripts/react_app/actions/ServerActions.js | ||
---|---|---|
errorThrown: errorThrown
|
||
}
|
||
});
|
||
},
|
||
receivedNotifications(response, textStatus, jqXHR) {
|
||
AppDispatcher.dispatch({
|
||
actionType: ACTIONS.RECEIVED_NOTIFICATIONS,
|
||
notifications: response.notifications
|
||
});
|
||
},
|
||
|
||
notificationsRequestError(jqXHR, textStatus, errorThrown) {
|
||
AppDispatcher.dispatch({
|
||
actionType: ACTIONS.NOTIFICATIONSS_REQUEST_ERROR, info: {
|
||
jqXHR: jqXHR,
|
||
textStatus: textStatus,
|
||
errorThrown: errorThrown
|
||
}
|
||
});
|
||
},
|
||
|
||
notificationMarkedAsRead(response, textStatus, jqXHR) {
|
||
AppDispatcher.dispatch(({
|
||
actionType: ACTIONS.NOTIFICATIONS_MARKED_AS_READ
|
||
}));
|
||
}
|
||
};
|
webpack/assets/javascripts/react_app/common/MountingService.js | ||
---|---|---|
import React from 'react';
|
||
import StatisticsChartsList from '../components/charts/StatisticsChartsList';
|
||
import PowerStatusContainer from '../components/hosts/PowerStatusContainer';
|
||
import NotificationDrawerToggle from '../components/notifications/NotificationDrawerToggle';
|
||
import NotificationDrawer from '../components/notifications/NotificationDrawer';
|
||
import ReactDOM from 'react-dom';
|
||
|
||
export function mount(component, selector, data) {
|
||
... | ... | |
PowerStatusContainer: {
|
||
type: PowerStatusContainer,
|
||
markup: <PowerStatusContainer url={data.url} id={data.id}/>
|
||
},
|
||
NotificationDrawerToggle: {
|
||
type: NotificationDrawerToggle,
|
||
markup: <NotificationDrawerToggle url={data.url}/>
|
||
},
|
||
NotificationDrawer: {
|
||
type: NotificationDrawer,
|
||
markup: <NotificationDrawer data={data} />
|
||
}
|
||
};
|
||
|
webpack/assets/javascripts/react_app/common/commonStyles.css | ||
---|---|---|
.pointer {
|
||
cursor: pointer;
|
||
}
|
webpack/assets/javascripts/react_app/common/testHelpers.js | ||
---|---|---|
export default {
|
||
mockStorage: () => {
|
||
let storage = {};
|
||
|
||
return {
|
||
setItem: (key, value) => {
|
||
storage[key] = value || '';
|
||
},
|
||
getItem: (key) => {
|
||
return key in storage ? storage[key] : null;
|
||
},
|
||
removeItem: (key) => {
|
||
delete storage[key];
|
||
},
|
||
get length() {
|
||
return Object.keys(storage).length;
|
||
},
|
||
key: (i) => {
|
||
let keys = Object.keys(storage);
|
||
|
||
return keys[i] || null;
|
||
}
|
||
};
|
||
}
|
||
};
|
webpack/assets/javascripts/react_app/components/common/Icon.js | ||
---|---|---|
import React from 'react';
|
||
import {ICON_CSS} from '../../constants';
|
||
|
||
const Icon = (props) => {
|
||
const classNames = props.css ? ICON_CSS[props.type] + ' ' + props.css : ICON_CSS[props.type];
|
||
|
||
return (
|
||
<span className={classNames}></span>
|
||
);
|
||
};
|
||
|
||
export default Icon;
|
webpack/assets/javascripts/react_app/components/common/Icon.test.js | ||
---|---|---|
jest.unmock('./Icon');
|
||
|
||
import React from 'react';
|
||
import { shallow } from 'enzyme';
|
||
import Icon from './Icon';
|
||
|
||
describe('Icon', () => {
|
||
it('displays icon css', () => {
|
||
const wrapper = shallow(<Icon type="ok" />);
|
||
|
||
expect(wrapper.html()).toEqual('<span class="pficon pficon-ok"></span>');
|
||
});
|
||
it('can receive additionl css classes', () => {
|
||
const wrapper = shallow(<Icon type="ok" css="pull-left" />);
|
||
|
||
expect(wrapper.html()).toEqual('<span class="pficon pficon-ok pull-left"></span>');
|
||
});
|
||
});
|
webpack/assets/javascripts/react_app/components/notifications/Notification.js | ||
---|---|---|
import React from 'react';
|
||
import Icon from '../common/Icon';
|
||
import moment from 'moment';
|
||
import NotificationActions from '../../actions/NotificationActions';
|
||
import '../../common/commonStyles.css';
|
||
|
||
/* eslint-disable camelcase */
|
||
const Notification = ({created_at, seen, text, level, id}) => {
|
||
const created = moment(created_at);
|
||
const title = __('Click to mark as read').toString();
|
||
const tooltip = {
|
||
title: title,
|
||
'data-toggle': 'tooltip',
|
||
'data-placement': 'top'
|
||
};
|
||
const markup = seen ?
|
||
(<span className="drawer-pf-notification-message">{text}</span>) :
|
||
(<strong {...tooltip} className="drawer-pf-notification-message pointer"
|
||
onClick={markAsRead}>{text}</strong>);
|
||
|
||
function markAsRead() {
|
||
NotificationActions.markAsRead('/notification_recipients/' + id);
|
||
}
|
||
|
||
window.tfm.tools.activateTooltips();
|
||
|
||
return (
|
||
<div className="drawer-pf-notification">
|
||
<Icon type={level} css="pull-left"></Icon>
|
||
{markup}
|
||
<div className="drawer-pf-notification-info">
|
||
<span className="date">{created.format('M/D/YY')}</span>
|
||
<span className="time">{created.format('hh:mm:ss A')}</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default Notification;
|
webpack/assets/javascripts/react_app/components/notifications/Notification.test.js | ||
---|---|---|
jest.unmock('./Notification');
|
||
|
||
import React from 'react';
|
||
import { mount } from 'enzyme';
|
||
import Notification from './Notification';
|
||
|
||
/* eslint-disable camelcase */
|
||
const notification = {
|
||
id: 1,
|
||
text: 'Job well done',
|
||
level: 'success',
|
||
created_at: '2016-12-13 16:52:47Z'
|
||
};
|
||
/* eslint-enable camelcase */
|
||
|
||
function setup(notification) {
|
||
return mount(<Notification {...notification} />);
|
||
}
|
||
|
||
describe('Notification', () => {
|
||
beforeEach(() => {
|
||
global.__ = (text) => text;
|
||
global.tfm = {
|
||
tools: {
|
||
activateTooltips: () => {}
|
||
}
|
||
};
|
||
});
|
||
|
||
it('displays text', () => {
|
||
const wrapper = setup(notification);
|
||
const messageElement = wrapper.find('.drawer-pf-notification-message');
|
||
|
||
expect(messageElement.text()).toBe('Job well done');
|
||
});
|
||
it('displays icon', () => {
|
||
const wrapper = setup(notification);
|
||
const iconElement = wrapper.find('.pficon.pficon-ok.pull-left');
|
||
|
||
expect(iconElement.length).toBe(1);
|
||
|
||
});
|
||
it('displays created date', () => {
|
||
const wrapper = setup(notification);
|
||
const dateElement = wrapper.find('.date');
|
||
|
||
expect(dateElement.text()).toBe('12/13/16');
|
||
});
|
||
xit('displays created time', () => {
|
||
const wrapper = setup(notification);
|
||
const timeElement = wrapper.find('.time');
|
||
|
||
expect(timeElement.text()).toBe('06:52:47 PM');
|
||
});
|
||
});
|
webpack/assets/javascripts/react_app/components/notifications/NotificationAccordion.js | ||
---|---|---|
import React from 'react';
|
||
import helpers from '../../common/helpers';
|
||
import NotificationPanel from './NotificationPanel';
|
||
import NotificationsStore from '../../stores/NotificationsStore';
|
||
import { ACTIONS } from '../../constants';
|
||
|
||
class NotificationAccordion extends React.Component {
|
||
constructor(props) {
|
||
super(props);
|
||
helpers.bindMethods(this, ['onChange']);
|
||
this.state = {expandedGroup: NotificationsStore.getExpandedGroup()};
|
||
}
|
||
|
||
componentDidMount() {
|
||
NotificationsStore.addChangeListener(this.onChange);
|
||
}
|
||
|
||
onChange(actionType) {
|
||
switch (actionType) {
|
||
case ACTIONS.NOTIFICATIONS_EXPAND_DRAWER_TAB: {
|
||
const expandedGroup = NotificationsStore.getExpandedGroup();
|
||
|
||
this.setState({ expandedGroup: expandedGroup });
|
||
break;
|
||
}
|
||
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
render() {
|
||
const notifications = this.props.notifications;
|
||
const keys = Object.keys(notifications);
|
||
|
||
let markup = keys.map((key) => {
|
||
return (
|
||
<NotificationPanel key={key} title={key} id={key} expandedGroup={this.state.expandedGroup}
|
||
notifications={this.props.notifications[key]} />
|
||
);
|
||
});
|
||
|
||
return (
|
||
<div className="panel-group" id="notification-drawer-accordion">
|
||
{markup}
|
||
</div>
|
||
);
|
||
}
|
||
}
|
||
|
||
export default NotificationAccordion;
|
webpack/assets/javascripts/react_app/components/notifications/NotificationAccordion.test.js | ||
---|---|---|
jest.unmock('./NotificationAccordion');
|
||
jest.unmock('../../stores/NotificationsStore');
|
||
|
||
import React from 'react';
|
||
import { shallow } from 'enzyme';
|
||
import NotificationAccordion from './NotificationAccordion';
|
||
import testData from '../../stores/NotificationsTestData';
|
||
import NotificationsStore from '../../stores/NotificationsStore';
|
||
|
||
function setup(data) {
|
||
return shallow(<NotificationAccordion notifications={data}/>);
|
||
}
|
||
|
||
describe('NotificationAccordion', () => {
|
||
it('runs a test', () => {
|
||
const data = NotificationsStore.prepareNotifications(testData);
|
||
|
||
// eslint-disable-next-line no-unused-vars
|
||
const wrapper = setup(data);
|
||
|
||
expect('not implemented').toBeTruthy();
|
||
});
|
||
});
|
webpack/assets/javascripts/react_app/components/notifications/NotificationDrawer.js | ||
---|---|---|
import React, { Component } from 'react';
|
||
import helpers from '../../common/helpers';
|
||
import NotificationsStore from '../../stores/NotificationsStore';
|
||
import NotificationDrawerTitle from './NotificationDrawerTitle';
|
||
import NotificationAccordion from './NotificationAccordion';
|
||
import {ACTIONS} from '../../constants';
|
||
|
||
class NotificationDrawer extends Component {
|
||
constructor(props) {
|
||
super(props);
|
||
this.state = {
|
||
notifications: NotificationsStore.getNotifications(),
|
||
drawerOpen: NotificationsStore.getIsDrawerOpen()
|
||
};
|
||
helpers.bindMethods(this, ['onChange']);
|
||
}
|
||
componentDidMount() {
|
||
NotificationsStore.addChangeListener(this.onChange);
|
||
}
|
||
onChange(actionType) {
|
||
switch (actionType) {
|
||
case ACTIONS.RECEIVED_NOTIFICATIONS: {
|
||
this.setState({ notifications: NotificationsStore.getNotifications() });
|
||
break;
|
||
}
|
||
case ACTIONS.NOTIFICATIONS_DRAWER_TOGGLE: {
|
||
const isOpen = NotificationsStore.getIsDrawerOpen();
|
||
|
||
this.setState({
|
||
drawerOpen: isOpen
|
||
});
|
||
break;
|
||
}
|
||
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
render() {
|
||
// render title and accordion
|
||
const toggleClass = this.state.drawerOpen ? '' : ' hide';
|
||
|
||
return (
|
||
<div className={'drawer-pf drawer-pf-notifications-non-clickable' + toggleClass}>
|
||
<NotificationDrawerTitle text="Notifications" />
|
||
<NotificationAccordion notifications={this.state.notifications} />
|
||
</div>
|
||
);
|
||
}
|
||
}
|
||
|
||
export default NotificationDrawer;
|
webpack/assets/javascripts/react_app/components/notifications/NotificationDrawer.test.js | ||
---|---|---|
jest.unmock('./NotificationDrawer');
|
||
|
||
import React from 'react';
|
||
import { shallow } from 'enzyme';
|
||
import NotificationDrawer from './NotificationDrawer';
|
||
import testHelpers from '../../common/testHelpers';
|
||
|
||
function setup() {
|
||
return shallow(<NotificationDrawer />);
|
||
}
|
||
|
||
describe('NotificationDrawer', () => {
|
||
beforeEach(() => {
|
||
global.sessionStorage = testHelpers.mockStorage();
|
||
});
|
||
|
||
it('runs a test', () => {
|
||
setup();
|
||
expect('not implemented').toBeTruthy();
|
||
});
|
||
});
|
webpack/assets/javascripts/react_app/components/notifications/NotificationDrawerTitle.js | ||
---|---|---|
import React from 'react';
|
||
|
||
const NotificationDrawerTitle = ({text}) => (
|
||
<div className="drawer-pf-title">
|
||
<a className="drawer-pf-toggle-expand"></a>
|
||
<h3 className="text-center">{text}</h3>
|
||
</div>
|
||
);
|
||
|
||
export default NotificationDrawerTitle;
|
webpack/assets/javascripts/react_app/components/notifications/NotificationDrawerTitle.test.js | ||
---|---|---|
jest.unmock('./NotificationDrawerTitle');
|
||
|
||
import React from 'react';
|
||
import { shallow } from 'enzyme';
|
||
import NotificationDrawerTitle from './NotificationDrawerTitle';
|
||
|
||
function setup(title) {
|
||
return shallow(<NotificationDrawerTitle text={title} />);
|
||
}
|
||
|
||
describe('NotificationDrawerTitle', () => {
|
||
it('displays title', () => {
|
||
const wrapper = setup('Drawer Title');
|
||
const title = wrapper.find('h3');
|
||
|
||
expect(title.text()).toBe('Drawer Title');
|
||
});
|
||
});
|
webpack/assets/javascripts/react_app/components/notifications/NotificationDrawerToggle.js | ||
---|---|---|
import React, { Component } from 'react';
|
||
import helpers from '../../common/helpers';
|
||
import NotificationsStore from '../../stores/NotificationsStore';
|
||
import NotificationActions from '../../actions/NotificationActions';
|
||
import { ACTIONS } from '../../constants';
|
||
|
||
class NotificationDrawerToggle extends Component {
|
||
constructor(props) {
|
||
super(props);
|
||
this.state = { count: 0, isLoaded: false, drawerOpen: false };
|
||
helpers.bindMethods(this, ['onChange', 'onClick']);
|
||
}
|
||
|
||
componentDidMount() {
|
||
NotificationsStore.addChangeListener(this.onChange);
|
||
// NotificationsStore.addErrorListener(this.onError);
|
||
NotificationActions.getNotifications(this.props.url);
|
||
}
|
||
|
||
onChange(actionType) {
|
||
switch (actionType) {
|
||
case ACTIONS.RECEIVED_NOTIFICATIONS: {
|
||
this.setState({
|
||
count: NotificationsStore.getNotifications().length,
|
||
isLoaded: true
|
||
});
|
||
break;
|
||
}
|
||
case ACTIONS.NOTIFICATIONS_DRAWER_TOGGLE: {
|
||
this.setState({
|
||
drawerOpen: NotificationsStore.getIsDrawerOpen()
|
||
});
|
||
break;
|
||
}
|
||
default:
|
||
break;
|
||
}
|
||
|
||
}
|
||
|
||
onClick() {
|
||
NotificationActions.toggleNotificationDrawer();
|
||
}
|
||
|
||
iconType() {
|
||
return this.state.count === 0 ? 'fa-bell-o' : 'fa-bell';
|
||
}
|
||
|
||
render() {
|
||
return (
|
||
<a className="nav-item-iconic drawer-pf-trigger-icon" onClick={this.onClick}>
|
||
<span className={'fa ' + this.iconType()} title={__('Notifications')}></span>
|
||
</a>
|
||
);
|
||
}
|
||
}
|
||
|
||
export default NotificationDrawerToggle;
|
webpack/assets/javascripts/react_app/components/notifications/NotificationDrawerToggle.test.js | ||
---|---|---|
jest.unmock('./NotificationDrawerToggle');
|
||
jest.unmock('../../stores/NotificationsStore');
|
||
import testHelpers from '../../common/testHelpers';
|
||
|
||
import React from 'react';
|
||
import { shallow } from 'enzyme';
|
||
import NotificationDrawerToggle from './NotificationDrawerToggle';
|
||
import NotificationsStore from '../../stores/NotificationsStore';
|
||
import NotificationActions from '../../actions/NotificationActions';
|
||
|
||
function setup() {
|
||
return shallow(<NotificationDrawerToggle />);
|
||
}
|
||
|
||
describe('NotificationDrawerToggle', () => {
|
||
beforeEach(() => {
|
||
global.__ = (text) => text;
|
||
global.sessionStorage = testHelpers.mockStorage();
|
||
});
|
||
|
||
it('stores show/hide status in store', () => {
|
||
NotificationActions.getNotifications = jest.fn();
|
||
|
||
const wrapper = setup();
|
||
|
||
expect(NotificationsStore.getIsDrawerOpen()).toBe(false);
|
||
|
||
wrapper.simulate('click');
|
||
|
||
expect(NotificationsStore.getIsDrawerOpen()).toBe(true);
|
||
|
||
wrapper.simulate('click');
|
||
|
||
expect(NotificationsStore.getIsDrawerOpen()).toBe(false);
|
||
});
|
||
});
|
webpack/assets/javascripts/react_app/components/notifications/NotificationPanel.js | ||
---|---|---|
import React from 'react';
|
||
import NotificationPanelHeading from './NotificationPanelHeading';
|
||
import NotificationPanelBody from './NotificationPanelBody';
|
||
|
||
const NotificationPanel = ({notifications, title, id, expandedGroup}) => {
|
||
let unread;
|
||
|
||
if (notifications) {
|
||
unread = notifications.reduce((total, curr) => {
|
||
if (!curr.seen) {
|
||
total = total + 1;
|
||
}
|
||
return total;
|
||
}, 0);
|
||
} else {
|
||
unread = 0;
|
||
}
|
||
|
||
return (
|
||
<div className="panel panel-default">
|
||
<NotificationPanelHeading title={title} group={id} unread={unread}/>
|
||
<NotificationPanelBody notifications={notifications}
|
||
group={id} expandedGroup={expandedGroup}/>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default NotificationPanel;
|
||
|
webpack/assets/javascripts/react_app/components/notifications/NotificationPanel.test.js | ||
---|---|---|
jest.unmock('./NotificationPanel');
|
||
|
||
import React from 'react';
|
||
import { shallow } from 'enzyme';
|
||
import NotificationPanel from './NotificationPanel';
|
||
|
||
function setup() {
|
||
return shallow(<NotificationPanel />);
|
||
}
|
||
|
||
describe('NotificationPanel', () => {
|
||
it('runs a test', () => {
|
||
setup();
|
||
expect('not implemented').toBeTruthy();
|
||
});
|
||
});
|
webpack/assets/javascripts/react_app/components/notifications/NotificationPanelBody.js | ||
---|---|---|
import React from 'react';
|
||
import Notification from './Notification';
|
||
import './Notifications.css';
|
||
|
||
const NotificationPanelBody = ({notifications, expandedGroup, group}) => {
|
||
let data;
|
||
let css = expandedGroup === group ? 'panel-body notification-panel-scroll' : 'hide';
|
||
|
||
if (notifications && notifications.length) {
|
||
data = notifications
|
||
.map(notification => <Notification key={notification.id} {...notification}></Notification>
|
||
);
|
||
} else {
|
||
data = null;
|
||
}
|
||
|
||
return (
|
||
<div className={css}>
|
||
{data}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default NotificationPanelBody;
|
webpack/assets/javascripts/react_app/components/notifications/NotificationPanelBody.test.js | ||
---|---|---|
jest.unmock('./NotificationPanelBody');
|
||
|
||
import React from 'react';
|
||
import { shallow } from 'enzyme';
|
||
import NotificationPanelBody from './NotificationPanelBody';
|
||
|
||
function setup() {
|
||
return shallow(<NotificationPanelBody />);
|
||
}
|
||
|
||
describe('NotificationPanelBody', () => {
|
||
it('runs a test', () => {
|
||
setup();
|
||
expect('not implemented').toBeTruthy();
|
||
});
|
||
});
|
webpack/assets/javascripts/react_app/components/notifications/NotificationPanelHeading.js | ||
---|---|---|
import React from 'react';
|
||
import NotificationActions from '../../actions/NotificationActions';
|
||
|
||
const NotificationPanelHeading = ({group, unread, title}) => {
|
||
const styles = {textTransform: 'capitalize'};
|
||
|
||
function expandDrawerTab() {
|
||
NotificationActions.expandDrawerTab(group);
|
||
}
|
||
|
||
return (
|
||
<div className="panel-heading" onClick={expandDrawerTab}>
|
||
<h4 className="panel-title">
|
||
<a style={styles}>
|
||
{title}
|
||
</a>
|
||
</h4>
|
||
<span className="panel-counter">
|
||
{unread} New {unread !== 1 ? 'Events' : 'Event'}
|
||
</span>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default NotificationPanelHeading;
|
webpack/assets/javascripts/react_app/components/notifications/NotificationPanelHeading.test.js | ||
---|---|---|
jest.unmock('./NotificationPanelHeading');
|
||
|
||
import React from 'react';
|
||
import { mount } from 'enzyme';
|
||
import NotificationPanelHeading from './NotificationPanelHeading';
|
||
|
||
function setup() {
|
||
return mount(<NotificationPanelHeading title="Panel Heading" unread="12" />);
|
||
}
|
||
|
||
describe('NotificationPanelHeading', () => {
|
||
it('shows title', () => {
|
||
const wrapper = setup();
|
||
|
||
expect(wrapper.find('.panel-title a').text()).toBe('Panel Heading');
|
||
});
|
||
it('shows number of unread', () => {
|
||
const wrapper = setup();
|
||
const counterText = wrapper.find('.panel-counter').text();
|
||
|
||
expect(counterText.substring(0, 2)).toBe('12');
|
||
});
|
||
});
|
webpack/assets/javascripts/react_app/components/notifications/Notifications.css | ||
---|---|---|
.notification-panel-scroll {
|
||
max-height: 364px;
|
||
overflow-y: auto;
|
||
}
|
webpack/assets/javascripts/react_app/constants.js | ||
---|---|---|
RECEIVED_STATISTICS: 'RECEIVED_STATISTICS',
|
||
STATISTICS_REQUEST_ERROR: 'STATISTICS_REQUEST_ERROR',
|
||
RECEIVED_HOSTS_POWER_STATE: 'RECEIVED_HOSTS_POWER_STATE',
|
||
HOSTS_REQUEST_ERROR: 'HOSTS_REQUEST_ERROR'
|
||
HOSTS_REQUEST_ERROR: 'HOSTS_REQUEST_ERROR',
|
||
RECEIVED_NOTIFICATIONS: 'RECEIVED_NOTIFICATIONS',
|
||
NOTIFICATIONS_REQUEST_ERROR: 'NOTIFICATIONS_REQUEST_ERROR',
|
||
NOTIFICATIONS_DRAWER_TOGGLE: 'NOTIFICATIONS_DRAWER_TOGGLE',
|
||
NOTIFICATIONS_EXPAND_DRAWER_TAB: 'NOTIFICATIONS_EXPAND_DRAWER_TAB',
|
||
NOTIFICATIONS_MARK_AS_READ: 'NOTIFICATIONS_MARK_AS_READ',
|
||
NOTIFICATIONS_MARKED_AS_READ: 'NOTIFICATIONS_MARKED_AS_READ'
|
||
};
|
||
|
||
export const STATUS = {
|
||
... | ... | |
RESOLVED: 'RESOLVED',
|
||
ERROR: 'ERROR'
|
||
};
|
||
|
||
export const ICON_CSS = {
|
||
ok: 'pficon pficon-ok',
|
||
success: 'pficon pficon-ok',
|
||
info: 'pficon pficon-info',
|
||
warning: 'pficon pficon-warning-triangle-o',
|
||
error: 'pficon pficon-error-circle-o'
|
||
};
|
webpack/assets/javascripts/react_app/stores/NotificationsStore.js | ||
---|---|---|
import AppDispatcher from '../dispatcher';
|
||
import { ACTIONS } from '../constants';
|
||
import AppEventEmitter from './AppEventEmitter';
|
||
import moment from 'moment';
|
||
|
||
let _notifications = {};
|
||
let _expandedTab = null;
|
||
|
||
class NotificationsEventEmitter extends AppEventEmitter {
|
||
constructor() {
|
||
super();
|
||
}
|
||
|
||
getNotifications() {
|
||
return (_notifications.data || []);
|
||
}
|
||
|
||
getIsDrawerOpen() {
|
||
const value = window.sessionStorage.getItem('isDrawerOpen') || 'false';
|
||
|
||
return JSON.parse(value);
|
||
}
|
||
|
||
getExpandedGroup() {
|
||
return _expandedTab;
|
||
}
|
||
|
||
prepareNotifications(notifications) {
|
||
let preparedData = {};
|
||
let sortedData = {};
|
||
let keys;
|
||
|
||
notifications.forEach((notification) => {
|
||
const group = notification.group;
|
||
const value = notification;
|
||
|
||
if (!preparedData[group]) {
|
||
preparedData[group] = [value];
|
||
} else {
|
||
preparedData[group].push(value);
|
||
}
|
||
});
|
||
|
||
keys = Object.keys(preparedData);
|
||
|
||
keys.forEach((key) => {
|
||
sortedData[key] = preparedData[key].sort(compare);
|
||
});
|
||
return sortedData;
|
||
}
|
||
}
|
||
|
||
const NotificationsStore = new NotificationsEventEmitter();
|
||
|
||
/* eslint-disable max-statements */
|
||
AppDispatcher.register(action => {
|
||
switch (action.actionType) {
|
||
case ACTIONS.RECEIVED_NOTIFICATIONS: {
|
||
_notifications.data = NotificationsStore.prepareNotifications(action.notifications);
|
||
|
||
NotificationsStore.emitChange(action.actionType);
|
||
break;
|
||
}
|
||
case ACTIONS.NOTIFICATIONS_REQUEST_ERROR: {
|
||
NotificationsStore.emitError(action.info);
|
||
break;
|
||
}
|
||
|
||
case ACTIONS.NOTIFICATIONS_DRAWER_TOGGLE: {
|
||
const value = NotificationsStore.getIsDrawerOpen();
|
||
|
||
window.sessionStorage.setItem('isDrawerOpen', JSON.stringify(!value));
|
||
NotificationsStore.emitChange(action.actionType);
|
||
break;
|
||
}
|
||
|
||
case ACTIONS.NOTIFICATIONS_EXPAND_DRAWER_TAB: {
|
||
if (_expandedTab === action.expand) {
|
||
_expandedTab = null;
|
||
} else {
|
||
_expandedTab = action.expand;
|
||
}
|
||
NotificationsStore.emitChange(action.actionType);
|
||
break;
|
||
}
|
||
|
||
default:
|
||
// no op
|
||
break;
|
||
}
|
||
});
|
||
|
||
/* eslint-enable max-statements */
|
||
|
||
export default NotificationsStore;
|
||
|
||
// sort notifications by time descending
|
||
function compare(a, b) {
|
||
const diff = moment(a.created_at) - moment(b.created_at);
|
||
let returnValue;
|
||
|
||
if (diff < 0) {
|
||
returnValue = 1;
|
||
} else if (diff > 0) {
|
||
returnValue = -1;
|
||
} else {
|
||
returnValue = 0;
|
||
}
|
||
return returnValue;
|
||
}
|
webpack/assets/javascripts/react_app/stores/NotificationsStore.test.js | ||
---|---|---|
jest.unmock('./NotificationsStore');
|
||
jest.unmock('../constants');
|
||
jest.unmock('../dispatcher');
|
||
jest.unmock('../actions/ServerActions');
|
||
|
||
import NotificationsStore from './NotificationsStore';
|
||
import data from './NotificationsTestData';
|
||
|
||
describe('NotificationsStore', () => {
|
||
it('builds data structure', () => {
|
||
const result = NotificationsStore.prepareNotifications(data);
|
||
|
||
expect(Object.keys(result).length).toBe(4);
|
||
expect(result.test.length).toBe(2);
|
||
expect(result.info.length).toBe(2);
|
||
expect(result.error.length).toBe(1);
|
||
expect(result.warning.length).toBe(1);
|
||
});
|
||
|
||
it('sorts groups by date descending', () => {
|
||
const result = NotificationsStore.prepareNotifications(data);
|
||
|
||
expect(result.test[0].id).toBe(6);
|
||
expect(result.test[1].id).toBe(3);
|
||
expect(result.info[0].id).toBe(18);
|
||
expect(result.info[1].id).toBe(9);
|
||
});
|
||
});
|
webpack/assets/javascripts/react_app/stores/NotificationsTestData.js | ||
---|---|---|
const NotificationsData = [
|
||
{
|
||
'id': 3,
|
||
'level': 'success',
|
||
'created_at': '2016-12-13T16:52:47.187Z',
|
||
'text': 'test me',
|
||
'group': 'test',
|
||
'seen': false
|
||
},
|
||
{
|
||
'id': 6,
|
||
'level': 'success',
|
||
'created_at': '2016-12-13T16:54:23.024Z',
|
||
'text': 'test me',
|
||
'group': 'test',
|
||
'seen': false
|
||
},
|
||
{
|
||
'id': 9,
|
||
'level': 'info',
|
||
'created_at': '2016-12-26T11:47:13.912Z',
|
||
'text': 'FYI',
|
||
'group': 'info',
|
||
'seen': false
|
||
},
|
||
{
|
||
'id': 12,
|
||
'level': 'error',
|
||
'created_at': '2016-12-26T11:48:59.433Z',
|
||
'text': 'No no no',
|
||
'group': 'error',
|
||
'seen': false
|
||
},
|
||
{
|
||
'id': 15,
|
||
'level': 'warning',
|
||
'created_at': '2016-12-26T11:49:08.164Z',
|
||
'text': 'Be careful',
|
||
'group': 'warning',
|
||
'seen': false
|
||
},
|
||
{
|
||
'id': 18,
|
||
'level': 'info',
|
||
'created_at': '2016-12-26T11:49:17.520Z',
|
||
'text': 'FYI',
|
||
'group': 'info',
|
||
'seen': false
|
||
}
|
||
];
|
||
|
||
export default NotificationsData;
|
Also available in: Unified diff
fixes #18010 - UI Notifications front-end implementation