feat(XOSAN): allow user to update packs (#2782)

This commit is contained in:
Pierre Donias 2018-05-15 16:11:04 +02:00 committed by Julien Fontanet
parent ebab7c0867
commit 114501ebc7
9 changed files with 693 additions and 622 deletions

View File

@ -3857,7 +3857,8 @@ export default {
xosanUsedSpace: 'Espace utilisé', xosanUsedSpace: 'Espace utilisé',
// Original text: "XOSAN pack needs to be installed on each host of the pool." // Original text: "XOSAN pack needs to be installed on each host of the pool."
xosanNeedPack: 'La pack XOSAN doit être installé sur tous les hôtes du pool.', xosanNeedPack:
'Le pack XOSAN doit être installé et à jour sur tous les hôtes du pool.',
// Original text: "Install it now!" // Original text: "Install it now!"
xosanInstallIt: 'Installer maintenant !', xosanInstallIt: 'Installer maintenant !',

View File

@ -1766,7 +1766,8 @@ const messages = {
xosanUsedSpace: 'Used space', xosanUsedSpace: 'Used space',
xosanLicense: 'License', xosanLicense: 'License',
xosanMultipleLicenses: 'This XOSAN has more than 1 license!', xosanMultipleLicenses: 'This XOSAN has more than 1 license!',
xosanNeedPack: 'XOSAN pack needs to be installed on each host of the pool.', xosanNeedPack:
'XOSAN pack needs to be installed and up to date on each host of the pool.',
xosanInstallIt: 'Install it now!', xosanInstallIt: 'Install it now!',
xosanNeedRestart: xosanNeedRestart:
'Some hosts need their toolstack to be restarted before you can create an XOSAN', 'Some hosts need their toolstack to be restarted before you can create an XOSAN',
@ -1794,6 +1795,14 @@ const messages = {
xosanPbdsDetached: 'Some SRs are detached from the XOSAN', xosanPbdsDetached: 'Some SRs are detached from the XOSAN',
xosanBadStatus: 'Something is wrong with: {badStatuses}', xosanBadStatus: 'Something is wrong with: {badStatuses}',
xosanRunning: 'Running', xosanRunning: 'Running',
xosanUpdatePacks: 'Update packs',
xosanPackUpdateChecking: 'Checking for updates',
xosanPackUpdateError:
'Error while checking XOSAN packs. Please make sure that the Cloud plugin is installed and loaded and that the updater is reachable.',
xosanPackUpdateUnavailable: 'XOSAN resources are unavailable',
xosanPackUpdateUnregistered: 'Not registered for XOSAN resources',
xosanPackUpdateUpToDate: "✓ This pool's XOSAN packs are up to date!",
xosanPackUpdateVersion: 'Update pool with latest pack v{version}',
xosanDelete: 'Delete XOSAN', xosanDelete: 'Delete XOSAN',
xosanFixIssue: 'Fix', xosanFixIssue: 'Fix',
xosanCreatingOn: 'Creating XOSAN on {pool}', xosanCreatingOn: 'Creating XOSAN on {pool}',
@ -1810,12 +1819,8 @@ const messages = {
xosanRegister: 'Register your appliance first', xosanRegister: 'Register your appliance first',
xosanLoading: 'Loading…', xosanLoading: 'Loading…',
xosanNotAvailable: 'XOSAN is not available at the moment', xosanNotAvailable: 'XOSAN is not available at the moment',
xosanInstallPackOnHosts: 'Install XOSAN pack on these hosts:',
xosanInstallPack: 'Install {pack} v{version}?',
xosanNoPackFound: xosanNoPackFound:
'No compatible XOSAN pack found for your XenServer versions.', 'No compatible XOSAN pack found for your XenServer versions.',
xosanPackRequirements:
'At least one of these version requirements must be satisfied by all the hosts in this pool:',
// SR tab XOSAN // SR tab XOSAN
xosanVmsNotRunning: 'Some XOSAN Virtual Machines are not running', xosanVmsNotRunning: 'Some XOSAN Virtual Machines are not running',
xosanVmsNotFound: 'Some XOSAN Virtual Machines could not be found', xosanVmsNotFound: 'Some XOSAN Virtual Machines could not be found',

View File

@ -20,6 +20,7 @@ import {
mapValues, mapValues,
replace, replace,
sample, sample,
some,
startsWith, startsWith,
} from 'lodash' } from 'lodash'
@ -28,6 +29,7 @@ import * as actions from './store/actions'
import invoke from './invoke' import invoke from './invoke'
import store from './store' import store from './store'
import { getObject } from './selectors' import { getObject } from './selectors'
import { satisfies as versionSatisfies } from 'semver'
export const EMPTY_ARRAY = Object.freeze([]) export const EMPTY_ARRAY = Object.freeze([])
export const EMPTY_OBJECT = Object.freeze({}) export const EMPTY_OBJECT = Object.freeze({})
@ -523,6 +525,40 @@ export const ShortDate = ({ timestamp }) => (
<FormattedDate value={timestamp} month='short' day='numeric' year='numeric' /> <FormattedDate value={timestamp} month='short' day='numeric' year='numeric' />
) )
export const findLatestPack = (packs, hostsVersions) => {
const checkVersion = version =>
!version ||
every(hostsVersions, hostVersion => versionSatisfies(hostVersion, version))
let latestPack = { version: '0' }
forEach(packs, pack => {
if (
pack.type === 'iso' &&
compareVersions(pack.version, '>', latestPack.version) &&
checkVersion(pack.requirements && pack.requirements.xenserver)
) {
latestPack = pack
}
})
if (latestPack.version === '0') {
// No compatible pack was found
return
}
return latestPack
}
export const isLatestXosanPackInstalled = (latestXosanPack, hosts) =>
latestXosanPack !== undefined &&
every(hosts, host =>
some(
host.supplementalPacks,
({ name, version }) =>
name === 'XOSAN' && version === latestXosanPack.version
)
)
// =================================================================== // ===================================================================
export const getMemoryUsedMetric = ({ memory, memoryFree = memory }) => export const getMemoryUsedMetric = ({ memory, memoryFree = memory }) =>

View File

@ -2412,20 +2412,6 @@ export const removeXosanBricks = (xosansr, bricks) =>
export const computeXosanPossibleOptions = (lvmSrs, brickSize) => export const computeXosanPossibleOptions = (lvmSrs, brickSize) =>
_call('xosan.computeXosanPossibleOptions', { lvmSrs, brickSize }) _call('xosan.computeXosanPossibleOptions', { lvmSrs, brickSize })
import InstallXosanPackModal from './install-xosan-pack-modal' // eslint-disable-line import/first
export const downloadAndInstallXosanPack = pool =>
confirm({
title: _('xosanInstallPackTitle', { pool: pool.name_label }),
icon: 'export',
body: <InstallXosanPackModal pool={pool} />,
}).then(pack =>
_call('xosan.downloadAndInstallXosanPack', {
id: pack.id,
version: pack.version,
pool: resolveId(pool),
})
)
export const registerXosan = () => export const registerXosan = () =>
_call('cloud.registerResource', { namespace: 'xosan' })::tap( _call('cloud.registerResource', { namespace: 'xosan' })::tap(
subscribeResourceCatalog.forceRefresh subscribeResourceCatalog.forceRefresh
@ -2434,6 +2420,31 @@ export const registerXosan = () =>
export const fixHostNotInXosanNetwork = (xosanSr, host) => export const fixHostNotInXosanNetwork = (xosanSr, host) =>
_call('xosan.fixHostNotInNetwork', { xosanSr, host }) _call('xosan.fixHostNotInNetwork', { xosanSr, host })
// XOSAN packs -----------------------------------------------------------------
export const getResourceCatalog = () => _call('cloud.getResourceCatalog')
const downloadAndInstallXosanPack = (pack, pool, { version }) =>
_call('xosan.downloadAndInstallXosanPack', {
id: resolveId(pack),
version,
pool: resolveId(pool),
})
import UpdateXosanPacksModal from './update-xosan-packs-modal' // eslint-disable-line import/first
export const updateXosanPacks = pool =>
confirm({
title: _('xosanUpdatePacks'),
icon: 'host-patch-update',
body: <UpdateXosanPacksModal pool={pool} />,
}).then(pack => {
if (pack === undefined) {
return
}
return downloadAndInstallXosanPack(pack, pool, { version: pack.version })
})
// Licenses -------------------------------------------------------------------- // Licenses --------------------------------------------------------------------
export const getLicenses = productId => _call('xoa.getLicenses', { productId }) export const getLicenses = productId => _call('xoa.getLicenses', { productId })

View File

@ -1,130 +0,0 @@
import _ from 'intl'
import Component from 'base-component'
import React from 'react'
import { connectStore, compareVersions, isXosanPack } from 'utils'
import { subscribeResourceCatalog, subscribePlugins } from 'xo'
import {
createGetObjectsOfType,
createSelector,
createCollectionWrapper,
} from 'selectors'
import { satisfies as versionSatisfies } from 'semver'
import { every, filter, forEach, map, some } from 'lodash'
const findLatestPack = (packs, hostsVersions) => {
const checkVersion = version =>
every(hostsVersions, hostVersion => versionSatisfies(hostVersion, version))
let latestPack = { version: '0' }
forEach(packs, pack => {
const xsVersionRequirement =
pack.requirements && pack.requirements.xenserver
if (
pack.type === 'iso' &&
compareVersions(pack.version, latestPack.version) > 0 &&
(!xsVersionRequirement || checkVersion(xsVersionRequirement))
) {
latestPack = pack
}
})
if (latestPack.version === '0') {
// No compatible pack was found
return
}
return latestPack
}
@connectStore(
() => ({
hosts: createGetObjectsOfType('host').filter(
createSelector(
(_, { pool }) => pool != null && pool.id,
poolId =>
poolId
? host =>
host.$pool === poolId &&
!some(host.supplementalPacks, isXosanPack)
: false
)
),
}),
{ withRef: true }
)
export default class InstallXosanPackModal extends Component {
componentDidMount () {
this._unsubscribePlugins = subscribePlugins(plugins =>
this.setState({ plugins })
)
this._unsubscribeResourceCatalog = subscribeResourceCatalog(catalog =>
this.setState({ catalog })
)
}
componentWillUnmount () {
this._unsubscribePlugins()
this._unsubscribeResourceCatalog()
}
_getXosanLatestPack = createSelector(
() => this.state.catalog && this.state.catalog.xosan,
createSelector(
() => this.props.hosts,
createCollectionWrapper(hosts => map(hosts, 'version'))
),
findLatestPack
)
_getXosanPacks = createSelector(
() => this.state.catalog && this.state.catalog.xosan,
packs => filter(packs, ({ type }) => type === 'iso')
)
get value () {
return this._getXosanLatestPack()
}
render () {
const { hosts } = this.props
const latestPack = this._getXosanLatestPack()
return (
<div>
{latestPack ? (
<div>
{_('xosanInstallPackOnHosts')}
<ul>
{map(hosts, host => <li key={host.id}>{host.name_label}</li>)}
</ul>
<div className='mt-1'>
{_('xosanInstallPack', {
pack: latestPack.name,
version: latestPack.version,
})}
</div>
</div>
) : (
<div>
{_('xosanNoPackFound')}
<br />
{_('xosanPackRequirements')}
<ul>
{map(this._getXosanPacks(), ({ name, requirements }, key) => (
<li key={key}>
{_.keyValue(
name,
requirements && requirements.xenserver
? requirements.xenserver
: '/'
)}
</li>
))}
</ul>
</div>
)}
</div>
)
}
}

View File

@ -0,0 +1,89 @@
import _ from 'intl'
import React from 'react'
import Component from 'base-component'
import { createGetObjectsOfType, createSelector } from 'selectors'
import { map } from 'lodash'
import { subscribeResourceCatalog } from 'xo'
import {
addSubscriptions,
isLatestXosanPackInstalled,
connectStore,
findLatestPack,
} from 'utils'
@connectStore(
{
hosts: createGetObjectsOfType('host').filter((_, { pool }) => host =>
host.$pool === pool.id
),
},
{ withRef: true }
)
@addSubscriptions(() => ({
catalog: subscribeResourceCatalog,
}))
export default class UpdateXosanPacksModal extends Component {
state = {
status: 'checking',
}
get value () {
return this.state.pack
}
_getStatus = createSelector(
() => this.props.catalog,
() => this.props.hosts,
(catalog, hosts) => {
if (catalog === undefined) {
return { status: 'error' }
}
if (catalog._namespaces.xosan === undefined) {
return { status: 'unavailable' }
}
if (!catalog._namespaces.xosan.registered) {
return { status: 'unregistered' }
}
const pack = findLatestPack(catalog.xosan, map(hosts, 'version'))
if (pack === undefined) {
return { status: 'noPack' }
}
if (isLatestXosanPackInstalled(pack, hosts)) {
return { status: 'upToDate' }
}
return { status: 'packFound', pack }
}
)
render () {
const { status, pack } = this._getStatus()
switch (status) {
case 'checking':
return <em>{_('xosanPackUpdateChecking')}</em>
case 'error':
return <em>{_('xosanPackUpdateError')}</em>
case 'unavailable':
return <em>{_('xosanPackUpdateUnavailable')}</em>
case 'unregistered':
return <em>{_('xosanPackUpdateUnregistered')}</em>
case 'noPack':
return <em>{_('xosanNoPackFound')}</em>
case 'upToDate':
return <em>{_('xosanPackUpdateUpToDate')}</em>
case 'packFound':
return (
<div>
{_('xosanPackUpdateVersion', {
version: pack.version,
})}
</div>
)
}
}
}

View File

@ -1,10 +1,11 @@
import _ from 'intl' import _ from 'intl'
import Component from 'base-component'
import Copiable from 'copiable' import Copiable from 'copiable'
import React from 'react' import React from 'react'
import TabButton from 'tab-button' import TabButton from 'tab-button'
import SelectFiles from 'select-files' import SelectFiles from 'select-files'
import Upgrade from 'xoa-upgrade' import Upgrade from 'xoa-upgrade'
import { connectStore } from 'utils' import { compareVersions, connectStore } from 'utils'
import { Toggle } from 'form' import { Toggle } from 'form'
import { import {
enableHost, enableHost,
@ -17,7 +18,7 @@ import {
import { FormattedRelative, FormattedTime } from 'react-intl' import { FormattedRelative, FormattedTime } from 'react-intl'
import { Container, Row, Col } from 'grid' import { Container, Row, Col } from 'grid'
import { createGetObjectsOfType, createSelector } from 'selectors' import { createGetObjectsOfType, createSelector } from 'selectors'
import { map, noop } from 'lodash' import { forEach, map, noop } from 'lodash'
const ALLOW_INSTALL_SUPP_PACK = process.env.XOA_PLAN > 1 const ALLOW_INSTALL_SUPP_PACK = process.env.XOA_PLAN > 1
@ -31,7 +32,9 @@ const formatPack = ({ name, author, description, version }, key) => (
</tr> </tr>
) )
export default connectStore(() => { const getPackId = ({ author, name }) => `${author}\0${name}`
@connectStore(() => {
const getPgpus = createGetObjectsOfType('PGPU') const getPgpus = createGetObjectsOfType('PGPU')
.pick((_, { host }) => host.$PGPUs) .pick((_, { host }) => host.$PGPUs)
.sort() .sort()
@ -44,7 +47,29 @@ export default connectStore(() => {
pcis: getPcis, pcis: getPcis,
pgpus: getPgpus, pgpus: getPgpus,
} }
})(({ host, pcis, pgpus }) => ( })
export default class extends Component {
_getPacks = createSelector(
() => this.props.host.supplementalPacks,
packs => {
const uniqPacks = {}
let packId, previousPack
forEach(packs, pack => {
packId = getPackId(pack)
if (
(previousPack = uniqPacks[packId]) === undefined ||
compareVersions(pack.version, previousPack.version) > 0
) {
uniqPacks[packId] = pack
}
})
return uniqPacks
}
)
render () {
const { host, pcis, pgpus } = this.props
return (
<Container> <Container>
<Row> <Row>
<Col className='text-xs-right'> <Col className='text-xs-right'>
@ -135,7 +160,9 @@ export default connectStore(() => {
<th>{_('hostStackStartedSince')}</th> <th>{_('hostStackStartedSince')}</th>
<td> <td>
{_('started', { {_('started', {
ago: <FormattedRelative value={host.agentStartTime * 1000} />, ago: (
<FormattedRelative value={host.agentStartTime * 1000} />
),
})} })}
</td> </td>
</tr> </tr>
@ -224,7 +251,7 @@ export default connectStore(() => {
<h3>{_('supplementalPacks')}</h3> <h3>{_('supplementalPacks')}</h3>
<table className='table'> <table className='table'>
<tbody> <tbody>
{map(host.supplementalPacks, formatPack)} {map(this._getPacks(), formatPack)}
{ALLOW_INSTALL_SUPP_PACK && ( {ALLOW_INSTALL_SUPP_PACK && (
<tr> <tr>
<th>{_('supplementalPackNew')}</th> <th>{_('supplementalPackNew')}</th>
@ -247,4 +274,6 @@ export default connectStore(() => {
</Col> </Col>
</Row> </Row>
</Container> </Container>
)) )
}
}

View File

@ -10,24 +10,13 @@ import Tooltip from 'tooltip'
import { Container, Col, Row } from 'grid' import { Container, Col, Row } from 'grid'
import { get } from 'xo-defined' import { get } from 'xo-defined'
import { ignoreErrors } from 'promise-toolbox' import { ignoreErrors } from 'promise-toolbox'
import { import { every, filter, find, flatten, forEach, isEmpty, map } from 'lodash'
every,
filter,
find,
flatten,
forEach,
isEmpty,
map,
mapValues,
some,
} from 'lodash'
import { createGetObjectsOfType, createSelector, isAdmin } from 'selectors' import { createGetObjectsOfType, createSelector, isAdmin } from 'selectors'
import { import {
addSubscriptions, addSubscriptions,
connectStore, connectStore,
cowSet, cowSet,
formatSize, formatSize,
isXosanPack,
ShortDate, ShortDate,
} from 'utils' } from 'utils'
import { import {
@ -37,6 +26,7 @@ import {
subscribePlugins, subscribePlugins,
subscribeResourceCatalog, subscribeResourceCatalog,
subscribeVolumeInfo, subscribeVolumeInfo,
updateXosanPacks,
} from 'xo' } from 'xo'
import NewXosan from './new-xosan' import NewXosan from './new-xosan'
@ -208,6 +198,12 @@ const XOSAN_COLUMNS = [
] ]
const XOSAN_INDIVIDUAL_ACTIONS = [ const XOSAN_INDIVIDUAL_ACTIONS = [
{
handler: (xosan, { pools }) => updateXosanPacks(pools[xosan.$pool]),
icon: 'host-patch-update',
label: _('xosanUpdatePacks'),
level: 'primary',
},
{ {
handler: deleteSr, handler: deleteSr,
icon: 'delete', icon: 'delete',
@ -221,14 +217,6 @@ const XOSAN_INDIVIDUAL_ACTIONS = [
const getHostsByPool = getHosts.groupBy('$pool') const getHostsByPool = getHosts.groupBy('$pool')
const getPools = createGetObjectsOfType('pool') const getPools = createGetObjectsOfType('pool')
const noPacksByPool = createSelector(getHostsByPool, hostsByPool =>
mapValues(
hostsByPool,
(poolHosts, poolId) =>
!every(poolHosts, host => some(host.supplementalPacks, isXosanPack))
)
)
const getPbdsBySr = createGetObjectsOfType('PBD').groupBy('SR') const getPbdsBySr = createGetObjectsOfType('PBD').groupBy('SR')
const getXosanSrs = createSelector( const getXosanSrs = createSelector(
createGetObjectsOfType('SR').filter([ createGetObjectsOfType('SR').filter([
@ -291,7 +279,6 @@ const XOSAN_INDIVIDUAL_ACTIONS = [
isAdmin, isAdmin,
isMasterOfflineByPool: getIsMasterOfflineByPool, isMasterOfflineByPool: getIsMasterOfflineByPool,
hostsNeedRestartByPool: getHostsNeedRestartByPool, hostsNeedRestartByPool: getHostsNeedRestartByPool,
noPacksByPool,
poolPredicate: getPoolPredicate, poolPredicate: getPoolPredicate,
pools: getPools, pools: getPools,
xoaRegistration: state => state.xoaRegisterState, xoaRegistration: state => state.xoaRegisterState,
@ -419,8 +406,8 @@ export default class Xosan extends Component {
const { const {
hostsNeedRestartByPool, hostsNeedRestartByPool,
isAdmin, isAdmin,
noPacksByPool,
poolPredicate, poolPredicate,
pools,
xoaRegistration, xoaRegistration,
xosanSrs, xosanSrs,
} = this.props } = this.props
@ -456,7 +443,6 @@ export default class Xosan extends Component {
(this._isXosanRegistered() ? ( (this._isXosanRegistered() ? (
<NewXosan <NewXosan
hostsNeedRestartByPool={hostsNeedRestartByPool} hostsNeedRestartByPool={hostsNeedRestartByPool}
noPacksByPool={noPacksByPool}
poolPredicate={poolPredicate} poolPredicate={poolPredicate}
onSrCreationFinished={this._updateLicenses} onSrCreationFinished={this._updateLicenses}
onSrCreationStarted={this._onSrCreationStarted} onSrCreationStarted={this._onSrCreationStarted}
@ -498,6 +484,7 @@ export default class Xosan extends Component {
isAdmin, isAdmin,
licensesByXosan: this._getLicensesByXosan(), licensesByXosan: this._getLicensesByXosan(),
licenseError, licenseError,
pools,
status: this.state.status, status: this.state.status,
}} }}
/> />

View File

@ -29,15 +29,18 @@ import {
} from 'selectors' } from 'selectors'
import { import {
addSubscriptions, addSubscriptions,
isLatestXosanPackInstalled,
compareVersions, compareVersions,
connectStore, connectStore,
findLatestPack,
formatSize, formatSize,
mapPlus, mapPlus,
} from 'utils' } from 'utils'
import { import {
computeXosanPossibleOptions, computeXosanPossibleOptions,
createXosanSR, createXosanSR,
downloadAndInstallXosanPack, updateXosanPacks,
getResourceCatalog,
restartHostsAgents, restartHostsAgents,
subscribeResourceCatalog, subscribeResourceCatalog,
} from 'xo' } from 'xo'
@ -76,14 +79,47 @@ export default class NewXosan extends Component {
suggestion: 0, suggestion: 0,
} }
_checkPacks = pool =>
getResourceCatalog().then(
catalog => {
if (catalog === undefined || catalog.xosan === undefined) {
this.setState({
checkPackError: true,
})
return
}
const hosts = filter(this.props.hosts, { $pool: pool.id })
const pack = findLatestPack(catalog.xosan, map(hosts, 'version'))
if (isLatestXosanPackInstalled(pack, hosts)) {
this.setState({
needsUpdate: true,
})
}
},
() => {
this.setState({
checkPackError: true,
})
}
)
_updateXosanPacks = pool =>
updateXosanPacks(pool).then(() => this._checkPacks(pool))
_selectPool = pool => { _selectPool = pool => {
this.setState({ this.setState({
selectedSrs: {},
brickSize: DEFAULT_BRICKSIZE, brickSize: DEFAULT_BRICKSIZE,
checkPackError: false,
memorySize: DEFAULT_MEMORY, memorySize: DEFAULT_MEMORY,
needsUpdate: false,
pif: undefined, pif: undefined,
pool, pool,
selectedSrs: {},
}) })
return this._checkPacks(pool)
} }
componentDidUpdate () { componentDidUpdate () {
@ -243,10 +279,12 @@ export default class NewXosan extends Component {
const { const {
brickSize, brickSize,
checkPackError,
customBrickSize, customBrickSize,
customIpRange, customIpRange,
ipRange, ipRange,
memorySize, memorySize,
needsUpdate,
pif, pif,
pool, pool,
selectedSrs, selectedSrs,
@ -256,12 +294,7 @@ export default class NewXosan extends Component {
vlan, vlan,
} = this.state } = this.state
const { const { hostsNeedRestartByPool, poolPredicate, notRegistered } = this.props
hostsNeedRestartByPool,
noPacksByPool,
poolPredicate,
notRegistered,
} = this.props
if (notRegistered) { if (notRegistered) {
return ( return (
@ -296,9 +329,7 @@ export default class NewXosan extends Component {
<Col size={4}> <Col size={4}>
<SelectPif <SelectPif
disabled={ disabled={
pool == null || pool == null || needsUpdate || !isEmpty(hostsNeedRestart)
noPacksByPool[pool.id] ||
!isEmpty(hostsNeedRestart)
} }
onChange={this.linkState('pif')} onChange={this.linkState('pif')}
predicate={this._getPifPredicate()} predicate={this._getPifPredicate()}
@ -307,22 +338,26 @@ export default class NewXosan extends Component {
</Col> </Col>
</Row> </Row>
{pool != null && {pool != null &&
noPacksByPool[pool.id] && ( (checkPackError ? (
<em>{_('xosanPackUpdateError')}</em>
) : needsUpdate ? (
<Row> <Row>
<Col>
<Icon icon='error' /> {_('xosanNeedPack')} <Icon icon='error' /> {_('xosanNeedPack')}
<br /> <br />
<ActionButton <ActionButton
btnStyle='success' btnStyle='success'
handler={downloadAndInstallXosanPack} handler={this._updateXosanPacks}
handlerParam={pool} handlerParam={pool}
icon='export' icon='export'
> >
{_('xosanInstallIt')} {_('xosanInstallIt')}
</ActionButton> </ActionButton>
</Col>
</Row> </Row>
)} ) : !isEmpty(hostsNeedRestart) ? (
{!isEmpty(hostsNeedRestart) && (
<Row> <Row>
<Col>
<Icon icon='error' /> {_('xosanNeedRestart')} <Icon icon='error' /> {_('xosanNeedRestart')}
<br /> <br />
<ActionButton <ActionButton
@ -333,12 +368,12 @@ export default class NewXosan extends Component {
> >
{_('xosanRestartAgents')} {_('xosanRestartAgents')}
</ActionButton> </ActionButton>
</Col>
</Row> </Row>
)} ) : (
{pool != null && [
!noPacksByPool[pool.id] &&
isEmpty(hostsNeedRestart) && [
<Row> <Row>
<Col>
<em>{_('xosanSelect2Srs')}</em> <em>{_('xosanSelect2Srs')}</em>
<table className='table table-striped'> <table className='table table-striped'>
<thead> <thead>
@ -380,9 +415,13 @@ export default class NewXosan extends Component {
<Tooltip <Tooltip
content={_('spaceLeftTooltip', { content={_('spaceLeftTooltip', {
used: String( used: String(
Math.round(sr.physical_usage / sr.size * 100) Math.round(
sr.physical_usage / sr.size * 100
)
),
free: formatSize(
sr.size - sr.physical_usage
), ),
free: formatSize(sr.size - sr.physical_usage),
})} })}
> >
<progress <progress
@ -398,8 +437,10 @@ export default class NewXosan extends Component {
})} })}
</tbody> </tbody>
</table> </table>
</Col>
</Row>, </Row>,
<Row> <Row>
<Col>
{!isEmpty(suggestions) && ( {!isEmpty(suggestions) && (
<div> <div>
<h3>{_('xosanSuggestions')}</h3> <h3>{_('xosanSuggestions')}</h3>
@ -548,6 +589,7 @@ export default class NewXosan extends Component {
<hr /> <hr />
</div> </div>
)} )}
</Col>
</Row>, </Row>,
<Row> <Row>
<Col> <Col>
@ -561,7 +603,8 @@ export default class NewXosan extends Component {
</ActionButton> </ActionButton>
</Col> </Col>
</Row>, </Row>,
]} ]
))}
<hr /> <hr />
</Container> </Container>
) )