feat(xo-web): www-xo notifications (#3904)

Related to xoa#21
This commit is contained in:
Pierre Donias 2019-01-28 15:29:52 +01:00 committed by Julien Fontanet
parent 0608cda6d7
commit 36f7af8576
9 changed files with 256 additions and 10 deletions

View File

@ -14,6 +14,7 @@
- [VM & Host] "Pool > Host" breadcrumb at the top of the page (PR [#3898](https://github.com/vatesfr/xen-orchestra/pull/3898))
- [Hosts] Ability to enable/disable host multipathing [#3659](https://github.com/vatesfr/xen-orchestra/issues/3659) (PR [#3865](https://github.com/vatesfr/xen-orchestra/pull/3865))
- [Login] Add OTP authentication [#2044](https://github.com/vatesfr/xen-orchestra/issues/2044) (PR [#3879](https://github.com/vatesfr/xen-orchestra/pull/3879))
- [Notifications] New notification page to provide important information about XOA (PR [#3904](https://github.com/vatesfr/xen-orchestra/pull/3904))
### Bug fixes
@ -28,6 +29,7 @@
### Released packages
- xoa-updater v0.15.0
- xen-api v0.24.1
- xo-vmdk-to-vhd v0.1.6
- xo-server v5.34.0

View File

@ -14,6 +14,13 @@ const messages = {
errorNoSuchItem: 'no such item',
errorUnknownItem: 'unknown {type}',
memoryFree: '{memoryFree} RAM free',
date: 'Date',
notifications: 'Notifications',
noNotifications: 'No notifications so far.',
notificationNew: 'NEW!',
messageSubject: 'Subject',
messageFrom: 'From',
messageReply: 'Reply',
editableLongClickPlaceholder: 'Long click to edit',
editableClickPlaceholder: 'Click to edit',
@ -74,6 +81,7 @@ const messages = {
xoaPage: 'XOA',
updatePage: 'Updates',
licensesPage: 'Licenses',
notificationsPage: 'Notifications',
settingsPage: 'Settings',
settingsServersPage: 'Servers',
settingsUsersPage: 'Users',

View File

@ -8,9 +8,13 @@ import ActionRowButton from './action-row-button'
export const CAN_REPORT_BUG = __DEV__ && process.env.XOA_PLAN > 1
export const reportBug = ({ formatMessage, message, title }) => {
const encodedTitle = encodeURIComponent(title)
const encodedTitle = encodeURIComponent(title == null ? '' : title)
const encodedMessage = encodeURIComponent(
formatMessage !== undefined ? formatMessage(message) : message
message == null
? ''
: formatMessage === undefined
? message
: formatMessage(message)
)
window.open(

View File

@ -5,6 +5,7 @@ import pFinally from 'promise-toolbox/finally'
import React from 'react'
import reflect from 'promise-toolbox/reflect'
import tap from 'promise-toolbox/tap'
import updater from 'xoa-updater'
import URL from 'url-parse'
import Xo from 'xo-lib'
import { createBackoff } from 'jsonrpc-websocket-client'
@ -358,6 +359,55 @@ export const subscribeResourceCatalog = createSubscription(() =>
_call('cloud.getResourceCatalog')
)
const getNotificationCookie = () => {
const notificationCookie = cookies.get(
`notifications:${store.getState().user.id}`
)
return notificationCookie === undefined ? {} : JSON.parse(notificationCookie)
}
const setNotificationCookie = (id, changes) => {
const notifications = getNotificationCookie()
notifications[id] = { ...(notifications[id] || {}), ...changes }
forEach(notifications[id], (value, key) => {
if (value === null) {
delete notifications[id][key]
}
})
cookies.set(
`notifications:${store.getState().user.id}`,
JSON.stringify(notifications)
)
}
export const dismissNotification = id => {
setNotificationCookie(id, { read: true, date: Date.now() })
subscribeNotifications.forceRefresh()
}
export const subscribeNotifications = createSubscription(async () => {
const { user, xoaUpdaterState } = store.getState()
if (
process.env.XOA_PLAN === 5 ||
xoaUpdaterState === 'disconnected' ||
xoaUpdaterState === 'error'
) {
return []
}
const notifications = await updater._call('getMessages')
const notificationCookie = getNotificationCookie()
return map(
user != null && user.permission === 'admin'
? notifications
: filter(notifications, { level: 'warning' }),
notification => ({
...notification,
read: !!get(notificationCookie, `${notification.id}.read`),
})
)
})
const checkSrCurrentStateSubscriptions = {}
export const subscribeCheckSrCurrentState = (pool, cb) => {
const poolId = resolveId(pool)

View File

@ -803,6 +803,10 @@
@extend .fa;
@extend .fa-file-text-o;
}
&-menu-notification {
@extend .fa;
@extend .fa-bell;
}
&-menu-settings {
@extend .fa;
@extend .fa-cog;
@ -1048,6 +1052,14 @@
@extend .fa;
@extend .fa-support;
}
&-notification {
@extend .fa;
@extend .fa-bell;
}
&-reply {
@extend .fa;
@extend .fa-share;
}
// XOSAN related

View File

@ -8,6 +8,7 @@ import map from 'lodash/map'
import React from 'react'
import Tooltip from 'tooltip'
import { UpdateTag } from '../xoa/update'
import { NotificationTag } from '../xoa/notifications'
import { addSubscriptions, connectStore, getXoaPlan, noop } from 'utils'
import {
connect,
@ -242,14 +243,38 @@ export default class Menu extends Component {
},
],
},
isAdmin && {
to: 'xoa/update',
{
to: isAdmin ? 'xoa/update' : 'xoa/notifications',
icon: 'menu-xoa',
label: 'xoa',
extra: <UpdateTag />,
extra: (
<span>
{isAdmin && (
<span>
<UpdateTag />{' '}
</span>
)}
<NotificationTag />
</span>
),
subMenu: [
{ to: 'xoa/update', icon: 'menu-update', label: 'updatePage' },
{ to: 'xoa/licenses', icon: 'menu-license', label: 'licensesPage' },
isAdmin && {
to: 'xoa/update',
icon: 'menu-update',
label: 'updatePage',
extra: <UpdateTag />,
},
isAdmin && {
to: 'xoa/licenses',
icon: 'menu-license',
label: 'licensesPage',
},
{
to: 'xoa/notifications',
icon: 'menu-notification',
label: 'notificationsPage',
extra: <NotificationTag />,
},
],
},
isAdmin && {
@ -535,7 +560,7 @@ const SubMenu = props => {
<li key={index} className='nav-item xo-menu-item'>
<Link activeClassName='active' className='nav-link' to={item.to}>
<Icon icon={`${item.icon}`} size='lg' fixedWidth />{' '}
{_(item.label)}
{_(item.label)} {item.extra}
</Link>
</li>
)

View File

@ -8,6 +8,7 @@ import { NavLink, NavTabs } from 'nav'
import Update from './update'
import Licenses from './licenses'
import Notifications, { NotificationTag } from './notifications'
const HEADER = (
<Container>
@ -25,6 +26,10 @@ const HEADER = (
<NavLink to='/xoa/licenses'>
<Icon icon='menu-license' /> {_('licensesPage')}
</NavLink>
<NavLink to='/xoa/notifications'>
<Icon icon='menu-notification' /> {_('notificationsPage')}{' '}
<NotificationTag />
</NavLink>
</NavTabs>
</Col>
</Row>
@ -34,6 +39,7 @@ const HEADER = (
const Xoa = routes('xoa', {
update: Update,
licenses: Licenses,
notifications: Notifications,
})(({ children }) =>
+process.env.XOA_PLAN === 5 ? (
<Container>

View File

@ -0,0 +1,139 @@
import _ from 'intl'
import classNames from 'classnames'
import decorate from 'apply-decorators'
import Icon from 'icon'
import marked from 'marked'
import NoObjects from 'no-objects'
import React from 'react'
import SortedTable from 'sorted-table'
import { addSubscriptions } from 'utils'
import { alert } from 'modal'
import { CAN_REPORT_BUG, reportBug } from 'report-bug-button'
import { filter, some } from 'lodash'
import { FormattedDate } from 'react-intl'
import { injectState, provideState } from 'reaclette'
import { subscribeNotifications, dismissNotification } from 'xo'
const COLUMNS = [
{
name: '',
itemRenderer: ({ level }) =>
level === 'warning' && <Icon icon='alarm' color='text-danger' />,
sortCriteria: 'level',
},
{
default: true,
name: _('date'),
itemRenderer: ({ created, read }) => {
const Tag = read ? 'span' : 'strong'
return (
<Tag>
<FormattedDate
value={new Date(created)}
month='long'
day='numeric'
year='numeric'
hour='2-digit'
minute='2-digit'
second='2-digit'
/>
</Tag>
)
},
sortCriteria: 'created',
sortOrder: 'desc',
},
{
name: _('messageFrom'),
itemRenderer: ({ read }) => {
const Tag = read ? 'span' : 'strong'
return <Tag>XO Team</Tag>
},
sortCriteria: '',
},
{
name: _('messageSubject'),
itemRenderer: ({ read, title }) => {
const Tag = read ? 'span' : 'strong'
return <Tag>{title}</Tag>
},
sortCriteria: 'title',
},
{
name: '',
itemRenderer: ({ id, read }) =>
!read && <strong className='text-success'>{_('notificationNew')}</strong>,
sortCriteria: 'read',
},
]
const ACTIONS = [
{
disabled: !CAN_REPORT_BUG,
label: _('messageReply'),
handler: notification =>
reportBug({
title: `Re: ${notification.title} (Ref: ${notification.id})`,
}),
icon: 'reply',
},
]
const Notifications = decorate([
addSubscriptions({
notifications: subscribeNotifications,
}),
provideState({
effects: {
showMessage: (effects, notification) => () =>
alert(
<span>
<Icon icon='notification' /> {notification.title}
</span>,
<div
dangerouslySetInnerHTML={{ __html: marked(notification.message) }}
/>
).then(() => dismissNotification(notification.id)),
},
}),
injectState,
({ notifications, effects }) => (
<NoObjects
collection={notifications}
columns={COLUMNS}
component={SortedTable}
emptyMessage={_('noNotifications')}
individualActions={ACTIONS}
rowAction={effects.showMessage}
stateUrlParam='s'
/>
),
])
export { Notifications as default }
export const NotificationTag = decorate([
addSubscriptions({
notifications: subscribeNotifications,
}),
provideState({
computed: {
nNewNotifications: (_, { notifications }) =>
filter(notifications, { read: false }).length,
someWarningNotifications: (_, { notifications }) =>
some(notifications, { level: 'warning', read: false }),
},
}),
injectState,
({ state }) =>
state.nNewNotifications > 0 ? (
<span
className={classNames(
'tag',
'tag-pill',
state.someWarningNotifications ? 'tag-danger' : 'tag-warning'
)}
>
{state.nNewNotifications}
</span>
) : null,
])

View File

@ -480,10 +480,10 @@ const COMPONENTS_BY_STATE = {
upgradeNeeded: (
<span className='fa-stack'>
<i className='fa fa-circle fa-stack-2x text-success' />
<i className='fa fa-bell fa-stack-1x' />
<i className='fa fa-refresh fa-stack-1x' />
</span>
),
upToDate: <Icon icon='success' />,
upToDate: null,
}
const TOOLTIPS_BY_STATE = {
connected: _('waitingUpdateInfo'),