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))
|
- [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))
|
- [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))
|
- [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
|
### Bug fixes
|
||||||
|
|
||||||
@ -28,6 +29,7 @@
|
|||||||
|
|
||||||
### Released packages
|
### Released packages
|
||||||
|
|
||||||
|
- xoa-updater v0.15.0
|
||||||
- xen-api v0.24.1
|
- xen-api v0.24.1
|
||||||
- xo-vmdk-to-vhd v0.1.6
|
- xo-vmdk-to-vhd v0.1.6
|
||||||
- xo-server v5.34.0
|
- xo-server v5.34.0
|
||||||
|
@ -14,6 +14,13 @@ const messages = {
|
|||||||
errorNoSuchItem: 'no such item',
|
errorNoSuchItem: 'no such item',
|
||||||
errorUnknownItem: 'unknown {type}',
|
errorUnknownItem: 'unknown {type}',
|
||||||
memoryFree: '{memoryFree} RAM free',
|
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',
|
editableLongClickPlaceholder: 'Long click to edit',
|
||||||
editableClickPlaceholder: 'Click to edit',
|
editableClickPlaceholder: 'Click to edit',
|
||||||
@ -74,6 +81,7 @@ const messages = {
|
|||||||
xoaPage: 'XOA',
|
xoaPage: 'XOA',
|
||||||
updatePage: 'Updates',
|
updatePage: 'Updates',
|
||||||
licensesPage: 'Licenses',
|
licensesPage: 'Licenses',
|
||||||
|
notificationsPage: 'Notifications',
|
||||||
settingsPage: 'Settings',
|
settingsPage: 'Settings',
|
||||||
settingsServersPage: 'Servers',
|
settingsServersPage: 'Servers',
|
||||||
settingsUsersPage: 'Users',
|
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 CAN_REPORT_BUG = __DEV__ && process.env.XOA_PLAN > 1
|
||||||
|
|
||||||
export const reportBug = ({ formatMessage, message, title }) => {
|
export const reportBug = ({ formatMessage, message, title }) => {
|
||||||
const encodedTitle = encodeURIComponent(title)
|
const encodedTitle = encodeURIComponent(title == null ? '' : title)
|
||||||
const encodedMessage = encodeURIComponent(
|
const encodedMessage = encodeURIComponent(
|
||||||
formatMessage !== undefined ? formatMessage(message) : message
|
message == null
|
||||||
|
? ''
|
||||||
|
: formatMessage === undefined
|
||||||
|
? message
|
||||||
|
: formatMessage(message)
|
||||||
)
|
)
|
||||||
|
|
||||||
window.open(
|
window.open(
|
||||||
|
@ -5,6 +5,7 @@ import pFinally from 'promise-toolbox/finally'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import reflect from 'promise-toolbox/reflect'
|
import reflect from 'promise-toolbox/reflect'
|
||||||
import tap from 'promise-toolbox/tap'
|
import tap from 'promise-toolbox/tap'
|
||||||
|
import updater from 'xoa-updater'
|
||||||
import URL from 'url-parse'
|
import URL from 'url-parse'
|
||||||
import Xo from 'xo-lib'
|
import Xo from 'xo-lib'
|
||||||
import { createBackoff } from 'jsonrpc-websocket-client'
|
import { createBackoff } from 'jsonrpc-websocket-client'
|
||||||
@ -358,6 +359,55 @@ export const subscribeResourceCatalog = createSubscription(() =>
|
|||||||
_call('cloud.getResourceCatalog')
|
_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 = {}
|
const checkSrCurrentStateSubscriptions = {}
|
||||||
export const subscribeCheckSrCurrentState = (pool, cb) => {
|
export const subscribeCheckSrCurrentState = (pool, cb) => {
|
||||||
const poolId = resolveId(pool)
|
const poolId = resolveId(pool)
|
||||||
|
@ -803,6 +803,10 @@
|
|||||||
@extend .fa;
|
@extend .fa;
|
||||||
@extend .fa-file-text-o;
|
@extend .fa-file-text-o;
|
||||||
}
|
}
|
||||||
|
&-menu-notification {
|
||||||
|
@extend .fa;
|
||||||
|
@extend .fa-bell;
|
||||||
|
}
|
||||||
&-menu-settings {
|
&-menu-settings {
|
||||||
@extend .fa;
|
@extend .fa;
|
||||||
@extend .fa-cog;
|
@extend .fa-cog;
|
||||||
@ -1048,6 +1052,14 @@
|
|||||||
@extend .fa;
|
@extend .fa;
|
||||||
@extend .fa-support;
|
@extend .fa-support;
|
||||||
}
|
}
|
||||||
|
&-notification {
|
||||||
|
@extend .fa;
|
||||||
|
@extend .fa-bell;
|
||||||
|
}
|
||||||
|
&-reply {
|
||||||
|
@extend .fa;
|
||||||
|
@extend .fa-share;
|
||||||
|
}
|
||||||
|
|
||||||
// XOSAN related
|
// XOSAN related
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import map from 'lodash/map'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Tooltip from 'tooltip'
|
import Tooltip from 'tooltip'
|
||||||
import { UpdateTag } from '../xoa/update'
|
import { UpdateTag } from '../xoa/update'
|
||||||
|
import { NotificationTag } from '../xoa/notifications'
|
||||||
import { addSubscriptions, connectStore, getXoaPlan, noop } from 'utils'
|
import { addSubscriptions, connectStore, getXoaPlan, noop } from 'utils'
|
||||||
import {
|
import {
|
||||||
connect,
|
connect,
|
||||||
@ -242,14 +243,38 @@ export default class Menu extends Component {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
isAdmin && {
|
{
|
||||||
to: 'xoa/update',
|
to: isAdmin ? 'xoa/update' : 'xoa/notifications',
|
||||||
icon: 'menu-xoa',
|
icon: 'menu-xoa',
|
||||||
label: 'xoa',
|
label: 'xoa',
|
||||||
extra: <UpdateTag />,
|
extra: (
|
||||||
|
<span>
|
||||||
|
{isAdmin && (
|
||||||
|
<span>
|
||||||
|
<UpdateTag />{' '}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<NotificationTag />
|
||||||
|
</span>
|
||||||
|
),
|
||||||
subMenu: [
|
subMenu: [
|
||||||
{ to: 'xoa/update', icon: 'menu-update', label: 'updatePage' },
|
isAdmin && {
|
||||||
{ to: 'xoa/licenses', icon: 'menu-license', label: 'licensesPage' },
|
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 && {
|
isAdmin && {
|
||||||
@ -535,7 +560,7 @@ const SubMenu = props => {
|
|||||||
<li key={index} className='nav-item xo-menu-item'>
|
<li key={index} className='nav-item xo-menu-item'>
|
||||||
<Link activeClassName='active' className='nav-link' to={item.to}>
|
<Link activeClassName='active' className='nav-link' to={item.to}>
|
||||||
<Icon icon={`${item.icon}`} size='lg' fixedWidth />{' '}
|
<Icon icon={`${item.icon}`} size='lg' fixedWidth />{' '}
|
||||||
{_(item.label)}
|
{_(item.label)} {item.extra}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
|
@ -8,6 +8,7 @@ import { NavLink, NavTabs } from 'nav'
|
|||||||
|
|
||||||
import Update from './update'
|
import Update from './update'
|
||||||
import Licenses from './licenses'
|
import Licenses from './licenses'
|
||||||
|
import Notifications, { NotificationTag } from './notifications'
|
||||||
|
|
||||||
const HEADER = (
|
const HEADER = (
|
||||||
<Container>
|
<Container>
|
||||||
@ -25,6 +26,10 @@ const HEADER = (
|
|||||||
<NavLink to='/xoa/licenses'>
|
<NavLink to='/xoa/licenses'>
|
||||||
<Icon icon='menu-license' /> {_('licensesPage')}
|
<Icon icon='menu-license' /> {_('licensesPage')}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
<NavLink to='/xoa/notifications'>
|
||||||
|
<Icon icon='menu-notification' /> {_('notificationsPage')}{' '}
|
||||||
|
<NotificationTag />
|
||||||
|
</NavLink>
|
||||||
</NavTabs>
|
</NavTabs>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@ -34,6 +39,7 @@ const HEADER = (
|
|||||||
const Xoa = routes('xoa', {
|
const Xoa = routes('xoa', {
|
||||||
update: Update,
|
update: Update,
|
||||||
licenses: Licenses,
|
licenses: Licenses,
|
||||||
|
notifications: Notifications,
|
||||||
})(({ children }) =>
|
})(({ children }) =>
|
||||||
+process.env.XOA_PLAN === 5 ? (
|
+process.env.XOA_PLAN === 5 ? (
|
||||||
<Container>
|
<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: (
|
upgradeNeeded: (
|
||||||
<span className='fa-stack'>
|
<span className='fa-stack'>
|
||||||
<i className='fa fa-circle fa-stack-2x text-success' />
|
<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>
|
</span>
|
||||||
),
|
),
|
||||||
upToDate: <Icon icon='success' />,
|
upToDate: null,
|
||||||
}
|
}
|
||||||
const TOOLTIPS_BY_STATE = {
|
const TOOLTIPS_BY_STATE = {
|
||||||
connected: _('waitingUpdateInfo'),
|
connected: _('waitingUpdateInfo'),
|
||||||
|
Loading…
Reference in New Issue
Block a user