Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(xo-web/home): icon grouping #6655

Merged
merged 19 commits into from
Mar 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- [Plugin/auth-oidc] Support `email` for _username field_ setting [Forum#59587](https://xcp-ng.org/forum/post/59587)
- [Plugin/auth-oidc] Well-known suffix is now optional in _auto-discovery URL_
- [PIF selector] Display the VLAN number when displaying a VLAN PIF [#4697](https://github.com/vatesfr/xen-orchestra/issues/4697) (PR [#6714](https://github.com/vatesfr/xen-orchestra/pull/6714))
- [Home/pool, host] Grouping of alert icons (PR [#6655](https://github.com/vatesfr/xen-orchestra/pull/6655))

### Bug fixes

Expand Down
63 changes: 63 additions & 0 deletions packages/xo-web/src/common/bulk-icons.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import _ from 'intl'
import PropTypes from 'prop-types'
import React from 'react'

import BaseComponent from './base-component'
import Icon from './icon'
import { alert } from './modal'
import { createSelector } from './selectors'

const ICON_WARNING_MODAL_LEVEL = {
danger: 1,
warning: 2,
success: 3,
}

class BulkIcons extends BaseComponent {
getSortedAlerts = createSelector(
() => this.props.alerts,
alerts =>
alerts?.sort((curr, next) => ICON_WARNING_MODAL_LEVEL[curr.level] - ICON_WARNING_MODAL_LEVEL[next.level]) ?? []
)

onClick = () =>
alert(
_('alerts'),
this.getSortedAlerts().map(({ level, render }, index) => (
<div className={`text-${level}`} key={index}>
{render}
</div>
))
)

render() {
const alerts = this.getSortedAlerts()
const length = alerts.length
const level = alerts[0]?.level

return (
length !== 0 && (
// <a> in order to bypass the BlockLink component
pdonias marked this conversation as resolved.
Show resolved Hide resolved
<a className='fa-stack' onClick={this.onClick} style={{ transform: 'scale(0.8)' }}>
<Icon icon='alarm' color={`text-${level}`} className='fa-stack-2x' />
{/* `fa-triangle` does not exist on FontAwesome4.`l` is used to fill the `!` of the `alarm` icon */}
Rajaa-BARHTAOUI marked this conversation as resolved.
Show resolved Hide resolved
<span className={`fa-stack-2x font-weight-bold text-${level}`} style={{ fontSize: '2.3em' }}>
l
</span>
<span className='fa-stack-1x text-white font-weight-bold'>{length}</span>
</a>
)
)
}
}

BulkIcons.propTypes = {
alerts: PropTypes.arrayOf(
PropTypes.exact({
level: PropTypes.string.isRequired,
render: PropTypes.element.isRequired,
})
),
}

export default BulkIcons
1 change: 1 addition & 0 deletions packages/xo-web/src/common/intl/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const forEach = require('lodash/forEach')

const messages = {
alpha: 'Alpha',
alerts: 'Alerts',
creation: 'Creation',
description: 'Description',
deleteSourceVm: 'Delete source VM',
Expand Down
88 changes: 68 additions & 20 deletions packages/xo-web/src/xo-app/home/host-item.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import _ from 'intl'
import Component from 'base-component'
import InconsistentHostTimeWarning from 'inconsistent-host-time-warning'
import Ellipsis, { EllipsisContainer } from 'ellipsis'
import Icon from 'icon'
import Link, { BlockLink } from 'link'
Expand All @@ -12,7 +11,16 @@ import HomeTags from 'home-tags'
import Tooltip from 'tooltip'
import { Row, Col } from 'grid'
import { Text } from 'editable'
import { addTag, editHost, fetchHostStats, removeTag, startHost, stopHost, subscribeHvSupportedVersions } from 'xo'
import {
addTag,
editHost,
fetchHostStats,
isHostTimeConsistentWithXoaTime,
removeTag,
startHost,
stopHost,
subscribeHvSupportedVersions,
} from 'xo'
import { addSubscriptions, connectStore, formatSizeShort, hasLicenseRestrictions, osFamily } from 'utils'
import {
createDoesHostNeedRestart,
Expand All @@ -23,9 +31,11 @@ import {
} from 'selectors'

import MiniStats from './mini-stats'
import LicenseWarning from '../host/license-warning'
import styles from './index.css'

import BulkIcons from '../../common/bulk-icons'
import { LICENSE_WARNING_BODY } from '../host/license-warning'

@addSubscriptions({
hvSupportedVersions: subscribeHvSupportedVersions,
})
Expand Down Expand Up @@ -66,6 +76,60 @@ export default class HostItem extends Component {
_toggleExpanded = () => this.setState({ expanded: !this.state.expanded })
_onSelect = () => this.props.onSelect(this.props.item.id)

_getAlerts = createSelector(
() => this.props.needsRestart,
() => this.props.item,
this._isMaintained,
(needsRestart, host, isMaintained) => {
const alerts = []

if (needsRestart) {
alerts.push({
level: 'warning',
render: (
<Link className='text-warning' to={`/hosts/${host.id}/patches`}>
<Icon icon='alarm' /> {_('rebootUpdateHostLabel')}
</Link>
),
})
}

if (!isMaintained) {
alerts.push({
level: 'warning',
render: (
<p>
<Icon icon='alarm' /> {_('noMoreMaintained')}
</p>
),
})
}

if (!isHostTimeConsistentWithXoaTime(host)) {
alerts.push({
level: 'danger',
render: (
<p>
<Icon icon='alarm' /> {_('warningHostTimeTooltip')}
</p>
),
})
}

if (hasLicenseRestrictions(host)) {
alerts.push({
level: 'danger',
render: (
Rajaa-BARHTAOUI marked this conversation as resolved.
Show resolved Hide resolved
<span>
<Icon icon='alarm' /> {_('licenseRestrictionsModalTitle')} {LICENSE_WARNING_BODY}
</span>
),
})
}
return alerts
}
)

render() {
const { container, expandAll, item: host, nVms, selected, state } = this.props

Expand Down Expand Up @@ -102,23 +166,7 @@ export default class HostItem extends Component {
<span className='tag tag-pill tag-info'>{_('pillMaster')}</span>
)}
&nbsp;
{this.props.needsRestart && (
<Tooltip content={_('rebootUpdateHostLabel')}>
<Link to={`/hosts/${host.id}/patches`}>
<Icon icon='alarm' />
</Link>
</Tooltip>
)}
&nbsp;
{!this._isMaintained() && (
<Tooltip content={_('noMoreMaintained')}>
<Icon className='text-warning' icon='alarm' />
</Tooltip>
)}
&nbsp;
<InconsistentHostTimeWarning host={host} />
pdonias marked this conversation as resolved.
Show resolved Hide resolved
&nbsp;
{hasLicenseRestrictions(host) && <LicenseWarning />}
<BulkIcons alerts={this._getAlerts()} />
</EllipsisContainer>
</Col>
<Col mediumSize={3} className='hidden-lg-down'>
Expand Down
51 changes: 43 additions & 8 deletions packages/xo-web/src/xo-app/home/pool-item.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ import { injectState } from 'reaclette'

import styles from './index.css'

import BulkIcons from '../../common/bulk-icons'
import { isAdmin } from '../../common/selectors'
import { ShortDate } from '../../common/utils'
import { getXoaPlan, SOURCES } from '../../common/xoa-plans'

@connectStore(() => {
const getPoolHosts = createGetObjectsOfType('host').filter(
Expand Down Expand Up @@ -72,28 +74,58 @@ export default class PoolItem extends Component {
this.props.missingPatches.then(patches => this.setState({ missingPatchCount: size(patches) }))
}

_getPoolLicenseIcon() {
_getPoolLicenseIconTooltip() {
const { state: reacletteState, item: pool } = this.props
let tooltip
const { icon, earliestExpirationDate, nHostsUnderLicense, nHosts, supportLevel } =
const { earliestExpirationDate, nHostsUnderLicense, nHosts, supportLevel } =
reacletteState.poolLicenseInfoByPoolId[pool.id]
let tooltip = _('poolNoSupport')

if (getXoaPlan() === SOURCES) {
tooltip = _('poolSupportSourceUsers')
}
if (supportLevel === 'total') {
tooltip = _('earliestExpirationDate', { dateString: <ShortDate timestamp={earliestExpirationDate} /> })
}
if (supportLevel === 'partial') {
tooltip = _('poolPartialSupport', { nHostsLicense: nHostsUnderLicense, nHosts })
}
return icon(tooltip)
return tooltip
}

_isXcpngPool() {
return Object.values(this.props.poolHosts)[0].productBrand === 'XCP-ng'
}

_getPoolLicenseInfo = () => this.props.state.poolLicenseInfoByPoolId[this.props.item.id]

_getAlerts = createSelector(
() => this.props.isAdmin,
this._getPoolLicenseInfo,
(isAdmin, poolLicenseInfo) => {
const alerts = []

if (isAdmin && this._isXcpngPool()) {
const { icon, supportLevel } = poolLicenseInfo
if (supportLevel !== 'total') {
const level = supportLevel === 'partial' ? 'warning' : 'danger'
alerts.push({
level,
render: (
<p>
{icon()} {this._getPoolLicenseIconTooltip()}
</p>
),
})
}
}
return alerts
}
)

render() {
const { item: pool, expandAll, isAdmin, selected, hostMetrics, poolHosts, nSrs, nVms } = this.props
const { item: pool, expandAll, selected, hostMetrics, poolHosts, nSrs, nVms } = this.props
const { missingPatchCount } = this.state
const { icon, supportLevel } = this._getPoolLicenseInfo()

return (
<div className={styles.item}>
Expand All @@ -106,16 +138,19 @@ export default class PoolItem extends Component {
<Ellipsis>
<Text value={pool.name_label} onChange={this._setNameLabel} useLongClick />
</Ellipsis>
{isAdmin && this._isXcpngPool() && <span className='ml-1'>{this._getPoolLicenseIcon()}</span>}
&nbsp;&nbsp;
&nbsp;
<BulkIcons alerts={this._getAlerts()} />
&nbsp;
{missingPatchCount > 0 && (
<span>
&nbsp;&nbsp;
<Tooltip content={_('homeMissingPatches')}>
<span className='tag tag-pill tag-danger'>{missingPatchCount}</span>
</Tooltip>
</span>
)}
&nbsp;
{isAdmin && this._isXcpngPool() && supportLevel === 'total' && icon(this._getPoolLicenseIconTooltip())}
&nbsp;
{pool.HA_enabled && (
<span>
&nbsp;&nbsp;
Expand Down
36 changes: 18 additions & 18 deletions packages/xo-web/src/xo-app/host/license-warning.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,24 @@ import Icon from 'icon'
import Tooltip from 'tooltip'
import { alert } from 'modal'

const showInfo = () =>
alert(
_('licenseRestrictionsModalTitle'),
<span>
<a href='https://xcp-ng.com/pricing.html#xcpngvsxenserver' rel='noopener noreferrer' target='_blank'>
{_('actionsRestricted')}
</a>{' '}
{_('counterRestrictionsOptions')}
<ul>
<li>
<a href='https://github.com/xcp-ng/xcp/wiki/Upgrade-from-XenServer' rel='noopener noreferrer' target='_blank'>
{_('counterRestrictionsOptionsXcp')}
</a>
</li>
<li>{_('counterRestrictionsOptionsXsLicense')}</li>
</ul>
</span>
)
export const LICENSE_WARNING_BODY = (
<span>
<a href='https://xcp-ng.com/pricing.html#xcpngvsxenserver' rel='noopener noreferrer' target='_blank'>
{_('actionsRestricted')}
</a>{' '}
{_('counterRestrictionsOptions')}
<ul>
<li>
<a href='https://github.com/xcp-ng/xcp/wiki/Upgrade-from-XenServer' rel='noopener noreferrer' target='_blank'>
{_('counterRestrictionsOptionsXcp')}
</a>
</li>
<li>{_('counterRestrictionsOptionsXsLicense')}</li>
</ul>
</span>
)

const showInfo = () => alert(_('licenseRestrictionsModalTitle'), LICENSE_WARNING_BODY)

const LicenseWarning = ({ iconSize = 'sm' }) => (
<Tooltip content={_('licenseRestrictions')}>
Expand Down
30 changes: 13 additions & 17 deletions packages/xo-web/src/xo-app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,27 +82,27 @@ const BODY_STYLE = {
width: '100%',
}

const WrapperIconPoolLicense = ({ children, tooltip }) => (
<Tooltip content={tooltip}>
<a href='https://xcp-ng.com' rel='noreferrer noopener' target='_blank'>
{children}
</a>
</Tooltip>
const WrapperIconPoolLicense = ({ children }) => (
<a href='https://xcp-ng.com' rel='noreferrer noopener' target='_blank'>
{children}
</a>
)

export const ICON_POOL_LICENSE = {
total: tooltip => (
<WrapperIconPoolLicense tooltip={tooltip}>
<Icon icon='pro-support' className='text-success' />
</WrapperIconPoolLicense>
<Tooltip content={tooltip}>
<WrapperIconPoolLicense>
<Icon icon='pro-support' className='text-success' />
</WrapperIconPoolLicense>
</Tooltip>
),
partial: tooltip => (
<WrapperIconPoolLicense tooltip={tooltip}>
partial: () => (
<WrapperIconPoolLicense>
<Icon icon='alarm' className='text-warning' />
</WrapperIconPoolLicense>
),
any: () => (
<WrapperIconPoolLicense tooltip={_('poolNoSupport')}>
<WrapperIconPoolLicense>
<Icon icon='alarm' className='text-danger' />
</WrapperIconPoolLicense>
),
Expand Down Expand Up @@ -187,11 +187,7 @@ export const ICON_POOL_LICENSE = {
if (getXoaPlan() === SOURCES.name) {
poolLicenseInfoByPoolId[poolId] = {
nHostsUnderLicense,
icon: () => (
<Tooltip content={_('poolSupportSourceUsers')}>
<Icon icon='unknown-status' className='text-danger' />
</Tooltip>
),
icon: () => <Icon icon='unknown-status' className='text-danger' />,
nHosts,
}
return
Expand Down