parent
0608cda6d7
commit
36f7af8576
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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>
|
||||
|
139
packages/xo-web/src/xo-app/xoa/notifications/index.js
Normal file
139
packages/xo-web/src/xo-app/xoa/notifications/index.js
Normal 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,
|
||||
])
|
@ -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'),
|
||||
|
Loading…
Reference in New Issue
Block a user