feat(xosan): license management (#2528)

This commit is contained in:
Pierre Donias 2017-12-29 16:48:21 +01:00 committed by Julien Fontanet
parent 8cb53b0c4e
commit 02c715e1cc
17 changed files with 1493 additions and 669 deletions

View File

@ -57,7 +57,9 @@ const messages = {
selfServicePage: 'Self service', selfServicePage: 'Self service',
backupPage: 'Backup', backupPage: 'Backup',
jobsPage: 'Jobs', jobsPage: 'Jobs',
xoaPage: 'XOA',
updatePage: 'Updates', updatePage: 'Updates',
licensesPage: 'Licenses',
settingsPage: 'Settings', settingsPage: 'Settings',
settingsServersPage: 'Servers', settingsServersPage: 'Servers',
settingsUsersPage: 'Users', settingsUsersPage: 'Users',
@ -95,6 +97,7 @@ const messages = {
jobsSchedulingPage: 'Scheduling', jobsSchedulingPage: 'Scheduling',
customJob: 'Custom Job', customJob: 'Custom Job',
userPage: 'User', userPage: 'User',
xoa: 'XOA',
// ----- Support ----- // ----- Support -----
noSupport: 'No support', noSupport: 'No support',
@ -1547,6 +1550,8 @@ const messages = {
xosanSrTitle: 'Xen Orchestra SAN SR', xosanSrTitle: 'Xen Orchestra SAN SR',
xosanAvailableSrsTitle: 'Select local SRs (lvm)', xosanAvailableSrsTitle: 'Select local SRs (lvm)',
xosanSuggestions: 'Suggestions', xosanSuggestions: 'Suggestions',
xosanDisperseWarning:
'Warning: using disperse layout is not recommended right now. Please read {link}.',
xosanName: 'Name', xosanName: 'Name',
xosanHost: 'Host', xosanHost: 'Host',
xosanHosts: 'Connected Hosts', xosanHosts: 'Connected Hosts',
@ -1554,6 +1559,8 @@ const messages = {
xosanVolumeId: 'Volume ID', xosanVolumeId: 'Volume ID',
xosanSize: 'Size', xosanSize: 'Size',
xosanUsedSpace: 'Used space', xosanUsedSpace: 'Used space',
xosanLicense: '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 on each host of the pool.',
xosanInstallIt: 'Install it now!', xosanInstallIt: 'Install it now!',
xosanNeedRestart: xosanNeedRestart:
@ -1595,11 +1602,9 @@ const messages = {
// Pack download modal // Pack download modal
xosanInstallCloudPlugin: 'Install cloud plugin first', xosanInstallCloudPlugin: 'Install cloud plugin first',
xosanLoadCloudPlugin: 'Load cloud plugin first', xosanLoadCloudPlugin: 'Load cloud plugin 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',
xosanRegisterBeta: 'Register for the XOSAN beta',
xosanSuccessfullyRegistered:
'You have successfully registered for the XOSAN beta. Please wait until your request has been approved.',
xosanInstallPackOnHosts: 'Install XOSAN pack on these hosts:', xosanInstallPackOnHosts: 'Install XOSAN pack on these hosts:',
xosanInstallPack: 'Install {pack} v{version}?', xosanInstallPack: 'Install {pack} v{version}?',
xosanNoPackFound: xosanNoPackFound:
@ -1641,11 +1646,53 @@ const messages = {
xosanRemove: 'Remove', xosanRemove: 'Remove',
xosanVolume: 'Volume', xosanVolume: 'Volume',
xosanVolumeOptions: 'Volume options', xosanVolumeOptions: 'Volume options',
xosanCouldNotFindVM: 'Could not find VM', xosanCouldNotFindVm: 'Could not find VM',
xosanUnderlyingStorageUsage: 'Using {usage}', xosanUnderlyingStorageUsage: 'Using {usage}',
xosanCustomIpNetwork: 'Custom IP network (/24)', xosanCustomIpNetwork: 'Custom IP network (/24)',
xosanIssueHostNotInNetwork: xosanIssueHostNotInNetwork:
'Will configure the host xosan network device with a static IP address and plug it in.', 'Will configure the host xosan network device with a static IP address and plug it in.',
// Licenses
licensesTitle: 'Licenses',
xosanUnregisteredDisclaimer:
'You are not registered and therefore will not be able to create or manage your XOSAN SRs. {link}',
xosanSourcesDisclaimer:
'In order to create a XOSAN SR, you need to use the Xen Orchestra Appliance and buy a XOSAN license on {link}.',
registerNow: 'Register now!',
licensesUnregisteredDisclaimer:
'You need to register your appliance to manage your licenses.',
licenseProduct: 'Product',
licenseBoundObject: 'Attached to',
licensePurchaser: 'Purchaser',
licenseExpires: 'Expires',
licensePurchaserYou: 'You',
productSupport: 'Support',
licenseNotBoundXosan: 'No XOSAN attached',
licenseBoundUnknownXosan: 'License attached to an unknown XOSAN',
licensesManage: 'Manage the licenses',
newLicense: 'New license',
refreshLicenses: 'Refresh',
xosanLicenseRestricted: 'Limited size because XOSAN is in trial',
xosanAdminNoLicenseDisclaimer:
'You need a license on this SR to manage the XOSAN.',
xosanAdminExpiredLicenseDisclaimer:
'Your XOSAN license has expired. You can still use the SR but cannot administrate it anymore.',
xosanCheckLicenseError: 'Could not check the license on this XOSAN SR',
xosanGetLicensesError: 'Could not fetch licenses',
xosanLicenseHasExpired: 'License has expired.',
xosanLicenseExpiresDate: 'License expires on {date}.',
xosanUpdateLicenseMessage: 'Update the license now!',
xosanUnknownSr: 'Unknown XOSAN SR.',
contactUs: 'Contact us!',
xosanNoLicense: 'No license.',
xosanUnlockNow: 'Unlock now!',
xosanBetaOverMessage:
'XOSAN Beta is over. You may now delete and create this storage again to be able to manage it.',
selectLicense: 'Select a license',
bindLicense: 'Bind license',
expiresOn: 'expires on {date}',
xosanInstallXoaPlugin: 'Install XOA plugin first',
xosanLoadXoaPlugin: 'Load XOA plugin first',
} }
forEach(messages, function (message, id) { forEach(messages, function (message, id) {
if (isString(message)) { if (isString(message)) {

View File

@ -3,6 +3,7 @@ import humanFormat from 'human-format'
import React from 'react' import React from 'react'
import ReadableStream from 'readable-stream' import ReadableStream from 'readable-stream'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { FormattedDate } from 'react-intl'
import { import {
clone, clone,
escapeRegExp, escapeRegExp,
@ -572,6 +573,9 @@ export const cowSet = (object, path, value, depth = 0) => {
// This function returns an estimated progress value between 0 and 1 // This function returns an estimated progress value between 0 and 1
// based on the elapsed time since the createFakeProgress call and // based on the elapsed time since the createFakeProgress call and
// the given estimated duration d // the given estimated duration d
//
// const getProgress = createFakeProgress(120)
// setInterval(() => console.log(`Progress: ${getProgress() * 100} %`), 1000)
export const createFakeProgress = (() => { export const createFakeProgress = (() => {
const S = 0.95 // Progress value after d seconds const S = 0.95 // Progress value after d seconds
return d => { return d => {
@ -582,3 +586,7 @@ export const createFakeProgress = (() => {
} }
} }
})() })()
export const ShortDate = ({ timestamp }) => (
<FormattedDate value={timestamp} month='short' day='numeric' year='numeric' />
)

View File

@ -328,6 +328,19 @@ export const subscribeCheckSrCurrentState = (pool, cb) => {
return checkSrCurrentStateSubscriptions[poolId](cb) return checkSrCurrentStateSubscriptions[poolId](cb)
} }
subscribeCheckSrCurrentState.forceRefresh = pool => {
if (pool === undefined) {
forEach(checkSrCurrentStateSubscriptions, subscription =>
subscription.forceRefresh()
)
return
}
const subscription = checkSrCurrentStateSubscriptions[resolveId(pool)]
if (subscription !== undefined) {
subscription.forceRefresh()
}
}
const missingPatchesByHost = {} const missingPatchesByHost = {}
export const subscribeHostMissingPatches = (host, cb) => { export const subscribeHostMissingPatches = (host, cb) => {
@ -2017,8 +2030,8 @@ export const createXosanSR = ({
brickSize, brickSize,
memorySize, memorySize,
ipRange, ipRange,
}) => }) => {
_call('xosan.createSR', { const promise = _call('xosan.createSR', {
template, template,
pif: pif.id, pif: pif.id,
vlan: String(vlan), vlan: String(vlan),
@ -2030,6 +2043,12 @@ export const createXosanSR = ({
ipRange, ipRange,
}) })
// Force refresh in parallel to get the creation progress sooner
subscribeCheckSrCurrentState.forceRefresh()
return promise
}
export const addXosanBricks = (xosansr, lvmsrs, brickSize) => export const addXosanBricks = (xosansr, lvmsrs, brickSize) =>
_call('xosan.addBricks', { xosansr, lvmsrs, brickSize }) _call('xosan.addBricks', { xosansr, lvmsrs, brickSize })
@ -2065,8 +2084,15 @@ export const downloadAndInstallXosanPack = pool =>
}) })
) )
export const registerXosan = namespace =>
_call('cloud.registerResource', { namespace: 'xosan' })
export const fixHostNotInXosanNetwork = (xosanSr, host) => export const fixHostNotInXosanNetwork = (xosanSr, host) =>
_call('xosan.fixHostNotInNetwork', { xosanSr, host }) _call('xosan.fixHostNotInNetwork', { xosanSr, host })
// Licenses --------------------------------------------------------------------
export const getLicenses = productId => _call('xoa.getLicenses', { productId })
export const getLicense = (productId, boundObjectId) =>
_call('xoa.getLicense', { productId, boundObjectId })
export const unlockXosan = (licenseId, srId) =>
_call('xosan.unlock', { licenseId, sr: srId })

View File

@ -744,10 +744,18 @@
@extend .fa-clock-o; @extend .fa-clock-o;
} }
} }
&-menu-xoa {
@extend .fa;
@extend .fa-cube;
}
&-menu-update { &-menu-update {
@extend .fa; @extend .fa;
@extend .fa-refresh; @extend .fa-refresh;
} }
&-menu-license {
@extend .fa;
@extend .fa-file-text-o;
}
&-menu-settings { &-menu-settings {
@extend .fa; @extend .fa;
@extend .fa-cog; @extend .fa-cog;
@ -985,6 +993,10 @@
@extend .fa; @extend .fa;
@extend .fa-star; @extend .fa-star;
} }
&-support {
@extend .fa;
@extend .fa-support;
}
// XOSAN related // XOSAN related

View File

@ -100,7 +100,7 @@ export default class About extends Component {
<div> <div>
<Row> <Row>
<Col> <Col>
<Link to={'/xoa-update'}> <Link to='/xoa-update'>
<h2>{_('freeTrial')}</h2> <h2>{_('freeTrial')}</h2>
{_('freeTrialNow')} {_('freeTrialNow')}
</Link> </Link>

View File

@ -38,7 +38,8 @@ import Tasks from './tasks'
import User from './user' import User from './user'
import Vm from './vm' import Vm from './vm'
import VmImport from './vm-import' import VmImport from './vm-import'
import XoaUpdates from './xoa-updates' import Xoa from './xoa'
import XoaUpdates from './xoa/update'
import Xosan from './xosan' import Xosan from './xosan'
import keymap, { help } from '../keymap' import keymap, { help } from '../keymap'
@ -87,7 +88,7 @@ const BODY_STYLE = {
'vms/import': VmImport, 'vms/import': VmImport,
'vms/new': NewVm, 'vms/new': NewVm,
'vms/:id': Vm, 'vms/:id': Vm,
'xoa-update': XoaUpdates, xoa: Xoa,
xosan: Xosan, xosan: Xosan,
}) })
@connectStore(state => { @connectStore(state => {

View File

@ -7,7 +7,7 @@ import Link from 'link'
import map from 'lodash/map' 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-updates' import { UpdateTag } from '../xoa/update'
import { addSubscriptions, connectStore, getXoaPlan, noop } from 'utils' import { addSubscriptions, connectStore, getXoaPlan, noop } from 'utils'
import { import {
connect, connect,
@ -203,10 +203,14 @@ export default class Menu extends Component {
], ],
}, },
isAdmin && { isAdmin && {
to: '/xoa-update', to: 'xoa/update',
icon: 'menu-update', icon: 'menu-xoa',
label: 'updatePage', label: 'xoa',
extra: <UpdateTag />, extra: <UpdateTag />,
subMenu: [
{ to: 'xoa/update', icon: 'menu-update', label: 'updatePage' },
{ to: 'xoa/licenses', icon: 'menu-license', label: 'licensesPage' },
],
}, },
isAdmin && { isAdmin && {
to: '/settings/servers', to: '/settings/servers',

View File

@ -14,10 +14,15 @@ export default class ReplaceBrickModalBody extends Component {
_getSrPredicate = createSelector( _getSrPredicate = createSelector(
() => this.props.vm, () => this.props.vm,
() => this.state.onSameVm, () => this.state.onSameVm,
(vm, onSameVm) => (vm, onSameVm) => {
onSameVm if (vm === undefined) {
return sr => sr.SR_type === 'lvm'
}
return onSameVm
? sr => sr.$container === vm.$container && sr.SR_type === 'lvm' ? sr => sr.$container === vm.$container && sr.SR_type === 'lvm'
: sr => sr.$pool === vm.$pool && sr.SR_type === 'lvm' : sr => sr.$pool === vm.$pool && sr.SR_type === 'lvm'
}
) )
_toggleOnSameVm = () => _toggleOnSameVm = () =>
@ -36,17 +41,19 @@ export default class ReplaceBrickModalBody extends Component {
render () { render () {
return ( return (
<Container> <Container>
<Row className='mb-1'> {this.props.vm !== undefined && (
<Col size={6}> <Row className='mb-1'>
<strong>{_('xosanOnSameVm')}</strong> <Col size={6}>
</Col> <strong>{_('xosanOnSameVm')}</strong>
<Col size={6}> </Col>
<Toggle <Col size={6}>
onChange={this._toggleOnSameVm} <Toggle
value={this.state.onSameVm} onChange={this._toggleOnSameVm}
/> value={this.state.onSameVm}
</Col> />
</Row> </Col>
</Row>
)}
<Row className='mb-1'> <Row className='mb-1'>
<Col size={6}> <Col size={6}>
<strong>{_('xosanUnderlyingStorage')}</strong> <strong>{_('xosanUnderlyingStorage')}</strong>

View File

@ -1,10 +1,11 @@
import _ from 'intl' import _ from 'intl'
import Component from 'base-component'
import HomeTags from 'home-tags' import HomeTags from 'home-tags'
import Icon from 'icon' import Icon from 'icon'
import map from 'lodash/map' import map from 'lodash/map'
import React from 'react' import React from 'react'
import Usage, { UsageElement } from 'usage' import Usage, { UsageElement } from 'usage'
import { addTag, removeTag } from 'xo' import { addTag, removeTag, getLicense } from 'xo'
import { connectStore, formatSize } from 'utils' import { connectStore, formatSize } from 'utils'
import { Container, Row, Col } from 'grid' import { Container, Row, Col } from 'grid'
import { createGetObject } from 'selectors' import { createGetObject } from 'selectors'
@ -20,76 +21,95 @@ const UsageTooltip = connectStore(() => ({
</span> </span>
)) ))
export default ({ sr, vdis, vdiSnapshots, unmanagedVdis }) => ( export default class TabGeneral extends Component {
<Container> componentDidMount () {
<Row className='text-xs-center'> const { sr } = this.props
<Col mediumSize={4}>
<h2> if (sr.SR_type === 'xosan') {
{sr.VDIs.length}x <Icon icon='disk' size='lg' /> getLicense('xosan.trial', sr.id).then(() =>
</h2> this.setState({ licenseRestriction: true })
</Col> )
<Col mediumSize={4}> }
<h2> }
{formatSize(sr.size)} <Icon icon='sr' size='lg' />
</h2> render () {
<p>Type: {sr.SR_type}</p> const { sr, vdis, vdiSnapshots, unmanagedVdis } = this.props
</Col>
<Col mediumSize={4}> return (
<h2> <Container>
{sr.$PBDs.length}x <Icon icon='host' size='lg' /> <Row className='text-xs-center'>
</h2> <Col mediumSize={4}>
</Col> <h2>
</Row> {sr.VDIs.length}x <Icon icon='disk' size='lg' />
<Row> </h2>
<Col className='text-xs-center'> </Col>
<h5> <Col mediumSize={4}>
{formatSize(sr.physical_usage)} {_('srUsed')} ({formatSize( <h2>
sr.size - sr.physical_usage {formatSize(sr.size)} <Icon icon='sr' size='lg' />
)}{' '} </h2>
{_('srFree')}) <p>Type: {sr.SR_type}</p>
</h5> {this.state.licenseRestriction && (
</Col> <p className='text-danger'>{_('xosanLicenseRestricted')}</p>
</Row> )}
<Row> </Col>
<Col smallOffset={1} mediumSize={10}> <Col mediumSize={4}>
<Usage total={sr.size}> <h2>
{map(unmanagedVdis, vdi => ( {sr.$PBDs.length}x <Icon icon='host' size='lg' />
<UsageElement </h2>
highlight </Col>
key={vdi.id} </Row>
tooltip={<UsageTooltip vdi={vdi} />} <Row>
value={vdi.usage} <Col className='text-xs-center'>
/> <h5>
))} {formatSize(sr.physical_usage)} {_('srUsed')} ({formatSize(
{map(vdis, vdi => ( sr.size - sr.physical_usage
<UsageElement )}{' '}
key={vdi.id} {_('srFree')})
tooltip={<UsageTooltip vdi={vdi} />} </h5>
value={vdi.usage} </Col>
/> </Row>
))} <Row>
{map(vdiSnapshots, vdi => ( <Col smallOffset={1} mediumSize={10}>
<UsageElement <Usage total={sr.size}>
highlight {map(unmanagedVdis, vdi => (
key={vdi.id} <UsageElement
tooltip={<UsageTooltip vdi={vdi} />} highlight
value={vdi.usage} key={vdi.id}
/> tooltip={<UsageTooltip vdi={vdi} />}
))} value={vdi.usage}
</Usage> />
</Col> ))}
</Row> {map(vdis, vdi => (
<Row className='text-xs-center'> <UsageElement
<Col> key={vdi.id}
<h2 className='text-xs-center'> tooltip={<UsageTooltip vdi={vdi} />}
<HomeTags value={vdi.usage}
type='SR' />
labels={sr.tags} ))}
onDelete={tag => removeTag(sr.id, tag)} {map(vdiSnapshots, vdi => (
onAdd={tag => addTag(sr.id, tag)} <UsageElement
/> highlight
</h2> key={vdi.id}
</Col> tooltip={<UsageTooltip vdi={vdi} />}
</Row> value={vdi.usage}
</Container> />
) ))}
</Usage>
</Col>
</Row>
<Row className='text-xs-center'>
<Col>
<h2 className='text-xs-center'>
<HomeTags
type='SR'
labels={sr.tags}
onDelete={tag => removeTag(sr.id, tag)}
onAdd={tag => addTag(sr.id, tag)}
/>
</h2>
</Col>
</Row>
</Container>
)
}
}

View File

@ -12,16 +12,18 @@ import { confirm } from 'modal'
import { error } from 'notification' import { error } from 'notification'
import { Toggle } from 'form' import { Toggle } from 'form'
import { Container, Col, Row } from 'grid' import { Container, Col, Row } from 'grid'
import { forEach, isEmpty, map, reduce, sum } from 'lodash' import { find, forEach, isEmpty, map, reduce, sum } from 'lodash'
import { createGetObjectsOfType, createSelector } from 'selectors' import { createGetObjectsOfType, createSelector, isAdmin } from 'selectors'
import { addSubscriptions, connectStore, formatSize } from 'utils' import { addSubscriptions, connectStore, formatSize } from 'utils'
import { import {
addXosanBricks, addXosanBricks,
getLicense,
fixHostNotInXosanNetwork, fixHostNotInXosanNetwork,
// TODO: uncomment when implementing subvolume deletion // TODO: uncomment when implementing subvolume deletion
// removeXosanBricks, // removeXosanBricks,
replaceXosanBrick, replaceXosanBrick,
startVm, startVm,
subscribePlugins,
subscribeVolumeInfo, subscribeVolumeInfo,
} from 'xo' } from 'xo'
@ -350,6 +352,7 @@ class Node extends Component {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@connectStore(() => ({ @connectStore(() => ({
isAdmin,
vms: createGetObjectsOfType('VM'), vms: createGetObjectsOfType('VM'),
hosts: createGetObjectsOfType('host'), hosts: createGetObjectsOfType('host'),
vbds: createGetObjectsOfType('VBD'), vbds: createGetObjectsOfType('VBD'),
@ -362,9 +365,22 @@ class Node extends Component {
subscribeVolumeInfo({ sr, infoType }, cb) subscribeVolumeInfo({ sr, infoType }, cb)
}) })
subscriptions.plugins = subscribePlugins
return subscriptions return subscriptions
}) })
export default class TabXosan extends Component { export default class TabXosan extends Component {
componentDidMount () {
const { id } = this.props.sr
getLicense('xosan', id)
.catch(() => getLicense('xosan.trial', id))
.then(
license => this.setState({ license }),
error => this.setState({ licenseError: error })
)
}
_addSubvolume = async () => { _addSubvolume = async () => {
const { srs, brickSize } = await confirm({ const { srs, brickSize } = await confirm({
icon: 'add', icon: 'add',
@ -425,6 +441,24 @@ export default class TabXosan extends Component {
// } // }
// ) // )
_getMissingXoaPlugin = createSelector(
() => this.props.plugins,
plugins => {
if (plugins === undefined) {
return _('xosanInstallXoaPlugin')
}
const xoaPlugin = find(plugins, { id: 'xoa' })
if (xoaPlugin === undefined) {
return _('xosanInstallXoaPlugin')
}
if (!xoaPlugin.loaded) {
return _('xosanLoadXoaPlugin')
}
}
)
_getConfig = createSelector( _getConfig = createSelector(
() => this.props.sr && this.props.sr.other_config['xo:xosan_config'], () => this.props.sr && this.props.sr.other_config['xo:xosan_config'],
otherConfig => (otherConfig ? JSON.parse(otherConfig) : null) otherConfig => (otherConfig ? JSON.parse(otherConfig) : null)
@ -582,13 +616,59 @@ export default class TabXosan extends Component {
) )
render () { render () {
const { showAdvanced } = this.state const { license, licenseError, showAdvanced } = this.state
const { heal_, info_, sr, status_, statusDetail_, vbds, vdis } = this.props const {
heal_,
info_,
sr,
status_,
statusDetail_,
vbds,
vdis,
isAdmin,
} = this.props
const missingXoaPlugin = this._getMissingXoaPlugin()
if (missingXoaPlugin !== undefined) {
return <em>{missingXoaPlugin}</em>
}
const xosanConfig = this._getConfig() const xosanConfig = this._getConfig()
if (
(license === undefined && licenseError === undefined) ||
xosanConfig === undefined
) {
return <em>{_('statusLoading')}</em>
}
if (!xosanConfig) { if (
return null licenseError !== undefined &&
licenseError.message !== 'No license found'
) {
return <span className='text-danger'>{_('xosanCheckLicenseError')}</span>
}
if (
licenseError !== undefined ||
(license !== undefined &&
license.productId !== 'xosan' &&
license.productId !== 'xosan.trial')
) {
return (
<span className='text-danger'>
{_('xosanAdminNoLicenseDisclaimer')}{' '}
{isAdmin && <Link to='/xoa/licenses'>{_('licensesManage')}</Link>}
</span>
)
}
if (license.expires < Date.now()) {
return (
<span className='text-danger'>
{_('xosanAdminExpiredLicenseDisclaimer')}{' '}
{isAdmin && <Link to='/xoa/licenses'>{_('licensesManage')}</Link>}
</span>
)
} }
if (!xosanConfig.version) { if (!xosanConfig.version) {

View File

@ -1,509 +0,0 @@
import _, { messages } from 'intl'
import ActionButton from 'action-button'
import AnsiUp from 'ansi_up'
import Button from 'button'
import Component from 'base-component'
import Icon from 'icon'
import Page from '../page'
import React from 'react'
import Tooltip from 'tooltip'
import xoaUpdater, { exposeTrial, isTrialRunning } from 'xoa-updater'
import { confirm } from 'modal'
import { connectStore } from 'utils'
import { Card, CardBlock, CardHeader } from 'card'
import { Container, Row, Col } from 'grid'
import { error } from 'notification'
import { injectIntl } from 'react-intl'
import { Password } from 'form'
import { serverVersion } from 'xo'
import { assign, includes, isEmpty, map } from 'lodash'
import pkg from '../../../package'
const ansiUp = new AnsiUp()
let updateSource
const promptForReload = (source, force) => {
if (force || (updateSource && source !== updateSource)) {
confirm({
title: _('promptUpgradeReloadTitle'),
body: <p>{_('promptUpgradeReloadMessage')}</p>,
}).then(() => window.location.reload())
}
updateSource = source
}
if (+process.env.XOA_PLAN < 5) {
xoaUpdater.start()
xoaUpdater.on('upgradeSuccessful', source => promptForReload(source, !source))
xoaUpdater.on('upToDate', promptForReload)
}
const HEADER = (
<Container>
<h2>
<Icon icon='menu-update' /> {_('updatePage')}
</h2>
</Container>
)
// FIXME: can't translate
const states = {
disconnected: 'Disconnected',
updating: 'Updating',
upgrading: 'Upgrading',
upToDate: 'Up to Date',
upgradeNeeded: 'Upgrade required',
registerNeeded: 'Registration required',
error: 'An error occured',
}
const update = () => xoaUpdater.update()
const upgrade = () => xoaUpdater.upgrade()
@connectStore(state => {
return {
configuration: state.xoaConfiguration,
log: state.xoaUpdaterLog,
registration: state.xoaRegisterState,
state: state.xoaUpdaterState,
trial: state.xoaTrialState,
}
})
@injectIntl
export default class XoaUpdates extends Component {
// These 3 inputs are "controlled" http://facebook.github.io/react/docs/forms.html#controlled-components
_handleProxyHostChange = event =>
this.setState({ proxyHost: event.target.value || '' })
_handleProxyPortChange = event =>
this.setState({ proxyPort: event.target.value || '' })
_handleProxyUserChange = event =>
this.setState({ proxyUser: event.target.value || '' })
_handleConfigReset = () => {
const { configuration } = this.props
const { proxyPassword } = this.refs
proxyPassword.value = ''
this.setState(configuration)
}
_register = async () => {
const { email, password } = this.state
const { registration } = this.props
const alreadyRegistered = registration.state === 'registered'
if (alreadyRegistered) {
try {
await confirm({
title: _('alreadyRegisteredModal'),
body: (
<p>
{_('alreadyRegisteredModalText', { email: registration.email })}
</p>
),
})
} catch (error) {
return
}
}
this.setState({ askRegisterAgain: false })
return xoaUpdater
.register(email, password, alreadyRegistered)
.then(() => this.setState({ email: '', password: '' }))
}
_configure = async () => {
const { proxyHost, proxyPort, proxyUser } = this.state
const { proxyPassword } = this.refs
return xoaUpdater
.configure({
proxyHost,
proxyPort,
proxyUser,
proxyPassword: proxyPassword.value,
})
.then(config => {
this.setState({
proxyHost: undefined,
proxyPort: undefined,
proxyUser: undefined,
})
proxyPassword.value = ''
})
}
_trialAllowed = trial => trial.state === 'default' && exposeTrial(trial.trial)
_trialAvailable = trial =>
trial.state === 'default' && isTrialRunning(trial.trial)
_trialConsumed = trial =>
trial.state === 'default' &&
!isTrialRunning(trial.trial) &&
!exposeTrial(trial.trial)
_updaterDown = trial => isEmpty(trial) || trial.state === 'ERROR'
_toggleAskRegisterAgain = () =>
this.setState({ askRegisterAgain: !this.state.askRegisterAgain })
_startTrial = async () => {
try {
await confirm({
title: _('trialReadyModal'),
body: <p>{_('trialReadyModalText')}</p>,
})
return xoaUpdater
.requestTrial()
.then(() => xoaUpdater.update())
.catch(err => error('Request Trial', err.message || String(err)))
} catch (_) {}
}
componentWillMount () {
this.setState({ askRegisterAgain: false })
serverVersion.then(serverVersion => {
this.setState({ serverVersion })
})
update()
}
render () {
const textClasses = {
info: 'text-info',
success: 'text-success',
warning: 'text-warning',
error: 'text-danger',
}
const { log, registration, state, trial } = this.props
let { configuration } = this.props // Configuration from the store
const alreadyRegistered = registration.state === 'registered'
configuration = assign({}, configuration)
const { proxyHost, proxyPort, proxyUser } = this.state // Edited non-saved configuration values override in view
let configEdited = false
proxyHost !== undefined &&
(configuration.proxyHost = proxyHost) &&
(configEdited = true)
proxyPort !== undefined &&
(configuration.proxyPort = proxyPort) &&
(configEdited = true)
proxyUser !== undefined &&
(configuration.proxyUser = proxyUser) &&
(configEdited = true)
const { formatMessage } = this.props.intl
return (
<Page header={HEADER} title='updateTitle' formatTitle>
<Container>
{+process.env.XOA_PLAN === 5 ? (
<div>
<h2 className='text-danger'>{_('noUpdaterCommunity')}</h2>
<p>
{_('considerSubscribe', {
link: (
<a href='https://xen-orchestra.com'>
https://xen-orchestra.com
</a>
),
})}
</p>
<p className='text-danger'>{_('noUpdaterWarning')}</p>
</div>
) : (
<div>
<Row>
<Col mediumSize={12}>
<Card>
<CardHeader>
<UpdateTag /> {states[state]}
</CardHeader>
<CardBlock>
<p>
{_('currentVersion')}{' '}
{`xo-server ${this.state.serverVersion}`} /{' '}
{`xo-web ${pkg.version}`}
</p>
{includes(['error', 'disconnected'], state) && (
<p>
<a href='https://xen-orchestra.com/docs/updater.html#troubleshooting'>
{_('updaterTroubleshootingLink')}
</a>
</p>
)}
<ActionButton
btnStyle='info'
handler={update}
icon='refresh'
>
{_('refresh')}
</ActionButton>{' '}
<ActionButton
btnStyle='success'
handler={upgrade}
icon='upgrade'
>
{_('upgrade')}
</ActionButton>
<hr />
<div>
{map(log, (log, key) => (
<p key={key}>
<span className={textClasses[log.level]}>
{log.date}
</span>:{' '}
<span
dangerouslySetInnerHTML={{
__html: ansiUp.ansi_to_html(log.message),
}}
/>
</p>
))}
</div>
</CardBlock>
</Card>
</Col>
</Row>
<Row>
<Col mediumSize={6}>
<Card>
<CardHeader>
{_('proxySettings')} {configEdited ? '*' : ''}
</CardHeader>
<CardBlock>
<form>
<fieldset>
<div className='form-group'>
<input
className='form-control'
placeholder={formatMessage(
messages.proxySettingsHostPlaceHolder
)}
type='text'
value={configuration.proxyHost}
onChange={this._handleProxyHostChange}
/>
</div>{' '}
<div className='form-group'>
<input
className='form-control'
placeholder={formatMessage(
messages.proxySettingsPortPlaceHolder
)}
type='text'
value={configuration.proxyPort}
onChange={this._handleProxyPortChange}
/>
</div>{' '}
<div className='form-group'>
<input
className='form-control'
placeholder={formatMessage(
messages.proxySettingsUsernamePlaceHolder
)}
type='text'
value={configuration.proxyUser}
onChange={this._handleProxyUserChange}
/>
</div>{' '}
<div className='form-group'>
<Password
placeholder={formatMessage(
messages.proxySettingsPasswordPlaceHolder
)}
ref='proxyPassword'
/>
</div>
</fieldset>
<br />
<fieldset>
<ActionButton
icon='save'
btnStyle='primary'
handler={this._configure}
>
{_('saveResourceSet')}
</ActionButton>{' '}
<Button
onClick={this._handleConfigReset}
disabled={!configEdited}
>
{_('resetResourceSet')}
</Button>
</fieldset>
</form>
</CardBlock>
</Card>
</Col>
<Col mediumSize={6}>
<Card>
<CardHeader>{_('registration')}</CardHeader>
<CardBlock>
<strong>{registration.state}</strong>
{registration.email && (
<span> to {registration.email}</span>
)}
<span className='text-danger'> {registration.error}</span>
{!alreadyRegistered || this.state.askRegisterAgain ? (
<form id='registrationForm'>
<div className='form-group'>
<input
className='form-control'
onChange={this.linkState('email')}
placeholder={formatMessage(
messages.updateRegistrationEmailPlaceHolder
)}
required
type='text'
/>
</div>{' '}
<div className='form-group'>
<Password
disabled={!this.state.email}
onChange={this.linkState('password')}
placeholder={formatMessage(
messages.updateRegistrationPasswordPlaceHolder
)}
required
/>
</div>{' '}
<ActionButton
form='registrationForm'
icon='success'
btnStyle='primary'
handler={this._register}
>
{_('register')}
</ActionButton>
</form>
) : (
<ActionButton
icon='edit'
btnStyle='primary'
handler={this._toggleAskRegisterAgain}
>
{_('editRegistration')}
</ActionButton>
)}
{+process.env.XOA_PLAN === 1 && (
<div>
<h2>{_('trial')}</h2>
{this._trialAllowed(trial) && (
<div>
{registration.state !== 'registered' && (
<p>{_('trialRegistration')}</p>
)}
{registration.state === 'registered' && (
<ActionButton
btnStyle='success'
handler={this._startTrial}
icon='trial'
>
{_('trialStartButton')}
</ActionButton>
)}
</div>
)}
{this._trialAvailable(trial) && (
<p className='text-success'>
{_('trialAvailableUntil', {
date: new Date(trial.trial.end),
})}
</p>
)}
{this._trialConsumed(trial) && (
<p>{_('trialConsumed')}</p>
)}
</div>
)}
{process.env.XOA_PLAN > 1 &&
process.env.XOA_PLAN < 5 && (
<div>
{trial.state === 'trustedTrial' && (
<p>{trial.message}</p>
)}
{trial.state === 'untrustedTrial' && (
<p className='text-danger'>{trial.message}</p>
)}
</div>
)}
{process.env.XOA_PLAN < 5 && (
<div>
{this._updaterDown(trial) && (
<p className='text-danger'>{_('trialLocked')}</p>
)}
</div>
)}
</CardBlock>
</Card>
</Col>
</Row>
</div>
)}
</Container>
</Page>
)
}
}
const UpdateAlarm = () => (
<span className='fa-stack'>
<i className='fa fa-circle fa-stack-2x text-danger' />
<i className='fa fa-exclamation fa-stack-1x' />
</span>
)
const UpdateError = () => (
<span className='fa-stack'>
<i className='fa fa-circle fa-stack-2x text-danger' />
<i className='fa fa-question fa-stack-1x' />
</span>
)
const UpdateWarning = () => (
<span className='fa-stack'>
<i className='fa fa-circle fa-stack-2x text-warning' />
<i className='fa fa-question fa-stack-1x' />
</span>
)
const UpdateSuccess = () => <Icon icon='success' />
const UpdateAlert = () => (
<span className='fa-stack'>
<i className='fa fa-circle fa-stack-2x text-success' />
<i className='fa fa-bell fa-stack-1x' />
</span>
)
const RegisterAlarm = () => (
<Icon icon='not-registered' className='text-warning' />
)
export const UpdateTag = connectStore(state => {
return {
configuration: state.xoaConfiguration,
log: state.xoaUpdaterLog,
registration: state.xoaRegisterState,
state: state.xoaUpdaterState,
trial: state.xoaTrialState,
}
})(props => {
const { state } = props
const components = {
disconnected: <UpdateError />,
connected: <UpdateWarning />,
upToDate: <UpdateSuccess />,
upgradeNeeded: <UpdateAlert />,
registerNeeded: <RegisterAlarm />,
error: <UpdateAlarm />,
}
const tooltips = {
disconnected: _('noUpdateInfo'),
connected: _('waitingUpdateInfo'),
upToDate: _('upToDate'),
upgradeNeeded: _('mustUpgrade'),
registerNeeded: _('registerNeeded'),
error: _('updaterError'),
}
return <Tooltip content={tooltips[state]}>{components[state]}</Tooltip>
})

58
src/xo-app/xoa/index.js Normal file
View File

@ -0,0 +1,58 @@
import _ from 'intl'
import Icon from 'icon'
import Page from '../page'
import React from 'react'
import { routes } from 'utils'
import { Container, Row, Col } from 'grid'
import { NavLink, NavTabs } from 'nav'
import Update from './update'
import Licenses from './licenses'
const HEADER = (
<Container>
<Row>
<Col mediumSize={3}>
<h2>
<Icon icon='menu-xoa' /> {_('xoaPage')}
</h2>
</Col>
<Col mediumSize={9}>
<NavTabs className='pull-right'>
<NavLink to={'/xoa/update'}>
<Icon icon='menu-xoa-update' /> {_('updatePage')}
</NavLink>
<NavLink to={'/xoa/licenses'}>
<Icon icon='menu-xoa-licenses' /> {_('licensesPage')}
</NavLink>
</NavTabs>
</Col>
</Row>
</Container>
)
const Xoa = routes('xoa', {
update: Update,
licenses: Licenses,
})(
({ children }) =>
+process.env.XOA_PLAN === 5 ? (
<Container>
<h2 className='text-danger'>{_('noUpdaterCommunity')}</h2>
<p>
{_('considerSubscribe', {
link: (
<a href='https://xen-orchestra.com'>https://xen-orchestra.com</a>
),
})}
</p>
<p className='text-danger'>{_('noUpdaterWarning')}</p>
</Container>
) : (
<Page header={HEADER} title='xoaPage' formatTitle>
{children}
</Page>
)
)
export default Xoa

View File

@ -0,0 +1,234 @@
import _ from 'intl'
import ActionButton from 'action-button'
import Component from 'base-component'
import Link from 'link'
import React from 'react'
import renderXoItem from 'render-xo-item'
import SortedTable from 'sorted-table'
import { Container, Row, Col } from 'grid'
import { createSelector, createGetObjectsOfType } from 'selectors'
import { find, forEach } from 'lodash'
import { addSubscriptions, connectStore, ShortDate } from 'utils'
import { subscribePlugins, getLicenses } from 'xo'
import { get } from 'xo-defined'
import Xosan from './xosan'
const openNewLicense = () => {
// FIXME: use link with target attribute
window.open('https://xen-orchestra.com/#!/member/purchaser')
}
const openSupport = () => {
// FIXME: use link with target attribute
window.open('https://xen-orchestra.com/#!/xosan-home/')
}
const PRODUCTS_COLUMNS = [
{
name: _('licenseProduct'),
itemRenderer: ({ product, id }) => (
<span>
{product} <span className='text-muted'>({id.slice(-4)})</span>
</span>
),
sortCriteria: ({ product, id }) => product + id.slice(-4),
default: true,
},
{
name: _('licenseBoundObject'),
itemRenderer: ({ renderBoundObject }) =>
renderBoundObject !== undefined && renderBoundObject(),
},
{
name: _('licensePurchaser'),
itemRenderer: ({ buyer }, { registeredEmail }) =>
buyer !== undefined ? (
buyer.email === registeredEmail ? (
_('licensePurchaserYou')
) : (
<a href={`mailto:${buyer.email}`}>{buyer.email}</a>
)
) : (
'-'
),
sortCriteria: 'buyer.email',
},
{
name: _('licenseExpires'),
itemRenderer: ({ expires }) =>
expires !== undefined ? <ShortDate timestamp={expires} /> : '-',
sortCriteria: 'expires',
sortOrder: 'desc',
},
]
const getBoundXosanRenderer = (boundObjectId, xosanSrs) => {
if (boundObjectId === undefined) {
return () => _('licenseNotBoundXosan')
}
const sr = xosanSrs[boundObjectId]
if (sr === undefined) {
return () => _('licenseBoundUnknownXosan')
}
return () => <Link to={`srs/${sr.id}`}>{renderXoItem(sr)}</Link>
}
@connectStore({
xosanSrs: createGetObjectsOfType('SR').filter([
({ SR_type }) => SR_type === 'xosan', // eslint-disable-line camelcase
]),
xoaRegistration: state => state.xoaRegisterState,
})
@addSubscriptions(() => ({
plugins: subscribePlugins,
}))
export default class Licenses extends Component {
constructor () {
super()
this.componentDidMount = this._updateLicenses
}
_updateLicenses = () =>
Promise.all([getLicenses('xosan'), getLicenses('xosan.trial')])
.then(([xosanLicenses, xosanTrialLicenses]) => {
this.setState({
xosanLicenses,
xosanTrialLicenses,
licenseError: undefined,
})
})
.catch(error => {
this.setState({ licenseError: error })
})
_getProducts = createSelector(
() => this.state.xosanLicenses,
() => this.props.xosanSrs,
(xosanLicenses, xosanSrs) => {
const products = []
if (get(() => xosanLicenses.state) === 'register-needed') {
// Should not happen
return
}
// XOSAN
const boundSrs = []
forEach(xosanLicenses, license => {
if (license.boundObjectId !== undefined) {
boundSrs.push(license.boundObjectId)
}
products.push({
product: 'XOSAN',
renderBoundObject: getBoundXosanRenderer(
license.boundObjectId,
xosanSrs
),
buyer: license.buyer,
expires: license.expires,
id: license.id,
})
})
return products
}
)
_getMissingXoaPlugin = createSelector(
() => this.props.plugins,
plugins => {
if (plugins === undefined) {
return true
}
const xoaPlugin = find(plugins, { id: 'xoa' })
if (!xoaPlugin) {
return _('xosanInstallXoaPlugin')
}
if (!xoaPlugin.loaded) {
return _('xosanLoadXoaPlugin')
}
}
)
render () {
if (get(() => this.props.xoaRegistration.state) !== 'registered') {
return (
<span>
<em>{_('licensesUnregisteredDisclaimer')}</em>{' '}
<Link to='xoa/update'>{_('registerNow')}</Link>
</span>
)
}
const missingXoaPlugin = this._getMissingXoaPlugin()
if (missingXoaPlugin !== undefined) {
return <em>{missingXoaPlugin}</em>
}
if (this.state.licenseError !== undefined) {
return <span className='text-danger'>{_('xosanGetLicensesError')}</span>
}
if (
this.state.xosanLicenses === undefined &&
this.state.xosanTrialLicenses === undefined
) {
return <em>{_('statusLoading')}</em>
}
return (
<Container>
<Row className='mb-1'>
<Col>
<ActionButton
btnStyle='success'
icon='add'
handler={openNewLicense}
>
{_('newLicense')}
</ActionButton>
<ActionButton
btnStyle='primary'
className='ml-1'
icon='refresh'
handler={this._updateLicenses}
>
{_('refreshLicenses')}
</ActionButton>
</Col>
</Row>
<Row>
<Col>
<SortedTable
collection={this._getProducts()}
columns={PRODUCTS_COLUMNS}
userData={{
registeredEmail: this.props.xoaRegistration.email,
}}
/>
</Col>
</Row>
<Row>
<Col>
<h2>
XOSAN
<ActionButton className='ml-1' handler={openSupport} icon='bug'>
{_('productSupport')}
</ActionButton>
</h2>
<Xosan
xosanLicenses={this.state.xosanLicenses}
xosanTrialLicenses={this.state.xosanTrialLicenses}
updateLicenses={this._updateLicenses}
/>
</Col>
</Row>
</Container>
)
}
}

View File

@ -0,0 +1,192 @@
import _ from 'intl'
import ActionButton from 'action-button'
import Component from 'base-component'
import Link from 'link'
import React from 'react'
import renderXoItem from 'render-xo-item'
import SortedTable from 'sorted-table'
import { connectStore } from 'utils'
import { createSelector, createGetObjectsOfType, createFilter } from 'selectors'
import { unlockXosan } from 'xo'
import { get } from 'xo-defined'
import { filter, forEach, includes, map } from 'lodash'
import { injectIntl } from 'react-intl'
@injectIntl
class SelectLicense extends Component {
state = { license: 'none' }
render () {
return (
<form className='form-inline'>
<select className='form-control' onChange={this.linkState('license')}>
{_('selectLicense', message => (
<option key='none' value='none'>
{message}
</option>
))}
{map(this.props.licenses, license =>
_(
'expiresOn',
{
date:
license.expires !== undefined
? this.props.intl.formatTime(license.expires, {
day: 'numeric',
month: 'numeric',
year: 'numeric',
})
: '',
},
message => (
<option key={license.id} value={license.id}>
{license.id.slice(-4)} {license.expires ? `(${message})` : ''}
</option>
)
)
)}
</select>
<ActionButton
btnStyle='primary'
className='ml-1'
disabled={this.state.license === 'none'}
handler={this.props.onChange}
handlerParam={get(() => this.state.license)}
icon='connect'
>
{_('bindLicense')}
</ActionButton>
</form>
)
}
}
const XOSAN_COLUMNS = [
{
name: _('xosanName'),
itemRenderer: sr => <Link to={`srs/${sr.id}`}>{renderXoItem(sr)}</Link>,
sortCriteria: 'name_label',
},
{
name: _('xosanPool'),
itemRenderer: (sr, { poolsBySr }) => {
const pool = poolsBySr[sr.id]
return <Link to={`pools/${pool.id}`}>{renderXoItem(pool)}</Link>
},
},
{
name: _('xosanLicense'),
itemRenderer: (
sr,
{ availableLicenses, licensesByXosan, updateLicenses }
) => {
const license = licensesByXosan[sr.id]
if (license === null) {
return (
<span className='text-danger'>
{_('xosanMultipleLicenses')}{' '}
<a href='https://xen-orchestra.com/'>{_('contactUs')}</a>
</span>
)
}
return license !== undefined ? (
license.id.slice(-4)
) : (
<SelectLicense
licenses={availableLicenses}
onChange={licenseId =>
unlockXosan(licenseId, sr.id).then(updateLicenses)
}
/>
)
},
},
]
const XOSAN_INDIVIDUAL_ACTIONS = [
{
label: _('productSupport'),
icon: 'support',
handler: () => window.open('https://xen-orchestra.com'),
},
]
@connectStore(() => {
const getXosanSrs = createGetObjectsOfType('SR').filter([
({ SR_type }) => SR_type === 'xosan', // eslint-disable-line camelcase
])
const getPoolsBySr = createSelector(
getXosanSrs,
createGetObjectsOfType('pool'),
(srs, pools) => {
const poolsBySr = {}
forEach(srs, sr => {
poolsBySr[sr.id] = pools[sr.$pool]
})
return poolsBySr
}
)
return {
xosanSrs: getXosanSrs,
poolsBySr: getPoolsBySr,
}
})
export default class Xosan extends Component {
_getLicensesByXosan = createSelector(
() => this.props.xosanLicenses,
licenses => {
const licensesByXosan = {}
forEach(licenses, license => {
let xosanId
if ((xosanId = license.boundObjectId) === undefined) {
return
}
licensesByXosan[xosanId] =
licensesByXosan[xosanId] !== undefined
? null // XOSAN bound to multiple licenses!
: license
})
return licensesByXosan
}
)
_getAvailableLicenses = createFilter(() => this.props.xosanLicenses, [
({ boundObjectId, expires }) =>
boundObjectId === undefined &&
(expires === undefined || expires > Date.now()),
])
_getKnownXosans = createSelector(
createSelector(
() => this.props.xosanLicenses,
() => this.props.xosanTrialLicenses,
(licenses = [], trialLicenses = []) =>
filter(map(licenses.concat(trialLicenses), 'boundObjectId'))
),
() => this.props.xosanSrs,
(knownXosanIds, xosanSrs) =>
filter(xosanSrs, ({ id }) => includes(knownXosanIds, id))
)
render () {
return (
<SortedTable
collection={this._getKnownXosans()}
columns={XOSAN_COLUMNS}
individualActions={XOSAN_INDIVIDUAL_ACTIONS}
userData={{
availableLicenses: this._getAvailableLicenses(),
licensesByXosan: this._getLicensesByXosan(),
poolsBySr: this.props.poolsBySr,
xosanSrs: this.props.xosanSrs,
updateLicenses: this.props.updateLicenses,
}}
/>
)
}
}

View File

@ -0,0 +1,468 @@
import _, { messages } from 'intl'
import ActionButton from 'action-button'
import AnsiUp from 'ansi_up'
import Button from 'button'
import Component from 'base-component'
import Icon from 'icon'
import React from 'react'
import Tooltip from 'tooltip'
import xoaUpdater, { exposeTrial, isTrialRunning } from 'xoa-updater'
import { confirm } from 'modal'
import { connectStore } from 'utils'
import { Card, CardBlock, CardHeader } from 'card'
import { Container, Row, Col } from 'grid'
import { error } from 'notification'
import { injectIntl } from 'react-intl'
import { Password } from 'form'
import { serverVersion } from 'xo'
import { assign, includes, isEmpty, map } from 'lodash'
import pkg from '../../../../package'
const ansiUp = new AnsiUp()
let updateSource
const promptForReload = (source, force) => {
if (force || (updateSource && source !== updateSource)) {
confirm({
title: _('promptUpgradeReloadTitle'),
body: <p>{_('promptUpgradeReloadMessage')}</p>,
}).then(() => window.location.reload())
}
updateSource = source
}
if (+process.env.XOA_PLAN < 5) {
xoaUpdater.start()
xoaUpdater.on('upgradeSuccessful', source => promptForReload(source, !source))
xoaUpdater.on('upToDate', promptForReload)
}
// FIXME: can't translate
const states = {
disconnected: 'Disconnected',
updating: 'Updating',
upgrading: 'Upgrading',
upToDate: 'Up to Date',
upgradeNeeded: 'Upgrade required',
registerNeeded: 'Registration required',
error: 'An error occured',
}
const update = () => xoaUpdater.update()
const upgrade = () => xoaUpdater.upgrade()
@connectStore(state => {
return {
configuration: state.xoaConfiguration,
log: state.xoaUpdaterLog,
registration: state.xoaRegisterState,
state: state.xoaUpdaterState,
trial: state.xoaTrialState,
}
})
@injectIntl
export default class XoaUpdates extends Component {
// These 3 inputs are "controlled" http://facebook.github.io/react/docs/forms.html#controlled-components
_handleProxyHostChange = event =>
this.setState({ proxyHost: event.target.value || '' })
_handleProxyPortChange = event =>
this.setState({ proxyPort: event.target.value || '' })
_handleProxyUserChange = event =>
this.setState({ proxyUser: event.target.value || '' })
_handleConfigReset = () => {
const { configuration } = this.props
const { proxyPassword } = this.refs
proxyPassword.value = ''
this.setState(configuration)
}
_register = async () => {
const { email, password } = this.state
const { registration } = this.props
const alreadyRegistered = registration.state === 'registered'
if (alreadyRegistered) {
try {
await confirm({
title: _('alreadyRegisteredModal'),
body: (
<p>
{_('alreadyRegisteredModalText', { email: registration.email })}
</p>
),
})
} catch (error) {
return
}
}
this.setState({ askRegisterAgain: false })
return xoaUpdater
.register(email, password, alreadyRegistered)
.then(() => this.setState({ email: '', password: '' }))
}
_configure = async () => {
const { proxyHost, proxyPort, proxyUser } = this.state
const { proxyPassword } = this.refs
return xoaUpdater
.configure({
proxyHost,
proxyPort,
proxyUser,
proxyPassword: proxyPassword.value,
})
.then(config => {
this.setState({
proxyHost: undefined,
proxyPort: undefined,
proxyUser: undefined,
})
proxyPassword.value = ''
})
}
_trialAllowed = trial => trial.state === 'default' && exposeTrial(trial.trial)
_trialAvailable = trial =>
trial.state === 'default' && isTrialRunning(trial.trial)
_trialConsumed = trial =>
trial.state === 'default' &&
!isTrialRunning(trial.trial) &&
!exposeTrial(trial.trial)
_updaterDown = trial => isEmpty(trial) || trial.state === 'ERROR'
_toggleAskRegisterAgain = () =>
this.setState({ askRegisterAgain: !this.state.askRegisterAgain })
_startTrial = async () => {
try {
await confirm({
title: _('trialReadyModal'),
body: <p>{_('trialReadyModalText')}</p>,
})
return xoaUpdater
.requestTrial()
.then(() => xoaUpdater.update())
.catch(err => error('Request Trial', err.message || String(err)))
} catch (_) {}
}
componentWillMount () {
this.setState({ askRegisterAgain: false })
serverVersion.then(serverVersion => {
this.setState({ serverVersion })
})
update()
}
render () {
const textClasses = {
info: 'text-info',
success: 'text-success',
warning: 'text-warning',
error: 'text-danger',
}
const { log, registration, state, trial } = this.props
let { configuration } = this.props // Configuration from the store
const alreadyRegistered = registration.state === 'registered'
configuration = assign({}, configuration)
const { proxyHost, proxyPort, proxyUser } = this.state // Edited non-saved configuration values override in view
let configEdited = false
proxyHost !== undefined &&
(configuration.proxyHost = proxyHost) &&
(configEdited = true)
proxyPort !== undefined &&
(configuration.proxyPort = proxyPort) &&
(configEdited = true)
proxyUser !== undefined &&
(configuration.proxyUser = proxyUser) &&
(configEdited = true)
const { formatMessage } = this.props.intl
return (
<Container>
<Row>
<Col mediumSize={12}>
<Card>
<CardHeader>
<UpdateTag /> {states[state]}
</CardHeader>
<CardBlock>
<p>
{_('currentVersion')}{' '}
{`xo-server ${this.state.serverVersion}`} /{' '}
{`xo-web ${pkg.version}`}
</p>
{includes(['error', 'disconnected'], state) && (
<p>
<a href='https://xen-orchestra.com/docs/updater.html#troubleshooting'>
{_('updaterTroubleshootingLink')}
</a>
</p>
)}
<ActionButton btnStyle='info' handler={update} icon='refresh'>
{_('refresh')}
</ActionButton>{' '}
<ActionButton
btnStyle='success'
handler={upgrade}
icon='upgrade'
>
{_('upgrade')}
</ActionButton>
<hr />
<div>
{map(log, (log, key) => (
<p key={key}>
<span className={textClasses[log.level]}>{log.date}</span>:{' '}
<span
dangerouslySetInnerHTML={{
__html: ansiUp.ansi_to_html(log.message),
}}
/>
</p>
))}
</div>
</CardBlock>
</Card>
</Col>
</Row>
<Row>
<Col mediumSize={6}>
<Card>
<CardHeader>
{_('proxySettings')} {configEdited ? '*' : ''}
</CardHeader>
<CardBlock>
<form>
<fieldset>
<div className='form-group'>
<input
className='form-control'
placeholder={formatMessage(
messages.proxySettingsHostPlaceHolder
)}
type='text'
value={configuration.proxyHost}
onChange={this._handleProxyHostChange}
/>
</div>{' '}
<div className='form-group'>
<input
className='form-control'
placeholder={formatMessage(
messages.proxySettingsPortPlaceHolder
)}
type='text'
value={configuration.proxyPort}
onChange={this._handleProxyPortChange}
/>
</div>{' '}
<div className='form-group'>
<input
className='form-control'
placeholder={formatMessage(
messages.proxySettingsUsernamePlaceHolder
)}
type='text'
value={configuration.proxyUser}
onChange={this._handleProxyUserChange}
/>
</div>{' '}
<div className='form-group'>
<Password
placeholder={formatMessage(
messages.proxySettingsPasswordPlaceHolder
)}
ref='proxyPassword'
/>
</div>
</fieldset>
<br />
<fieldset>
<ActionButton
icon='save'
btnStyle='primary'
handler={this._configure}
>
{_('saveResourceSet')}
</ActionButton>{' '}
<Button
onClick={this._handleConfigReset}
disabled={!configEdited}
>
{_('resetResourceSet')}
</Button>
</fieldset>
</form>
</CardBlock>
</Card>
</Col>
<Col mediumSize={6}>
<Card>
<CardHeader>{_('registration')}</CardHeader>
<CardBlock>
<strong>{registration.state}</strong>
{registration.email && <span> to {registration.email}</span>}
<span className='text-danger'> {registration.error}</span>
{!alreadyRegistered || this.state.askRegisterAgain ? (
<form id='registrationForm'>
<div className='form-group'>
<input
className='form-control'
onChange={this.linkState('email')}
placeholder={formatMessage(
messages.updateRegistrationEmailPlaceHolder
)}
required
type='text'
/>
</div>{' '}
<div className='form-group'>
<Password
disabled={!this.state.email}
onChange={this.linkState('password')}
placeholder={formatMessage(
messages.updateRegistrationPasswordPlaceHolder
)}
required
/>
</div>{' '}
<ActionButton
form='registrationForm'
icon='success'
btnStyle='primary'
handler={this._register}
>
{_('register')}
</ActionButton>
</form>
) : (
<ActionButton
icon='edit'
btnStyle='primary'
handler={this._toggleAskRegisterAgain}
>
{_('editRegistration')}
</ActionButton>
)}
{+process.env.XOA_PLAN === 1 && (
<div>
<h2>{_('trial')}</h2>
{this._trialAllowed(trial) && (
<div>
{registration.state !== 'registered' && (
<p>{_('trialRegistration')}</p>
)}
{registration.state === 'registered' && (
<ActionButton
btnStyle='success'
handler={this._startTrial}
icon='trial'
>
{_('trialStartButton')}
</ActionButton>
)}
</div>
)}
{this._trialAvailable(trial) && (
<p className='text-success'>
{_('trialAvailableUntil', {
date: new Date(trial.trial.end),
})}
</p>
)}
{this._trialConsumed(trial) && <p>{_('trialConsumed')}</p>}
</div>
)}
{process.env.XOA_PLAN > 1 &&
process.env.XOA_PLAN < 5 && (
<div>
{trial.state === 'trustedTrial' && <p>{trial.message}</p>}
{trial.state === 'untrustedTrial' && (
<p className='text-danger'>{trial.message}</p>
)}
</div>
)}
{process.env.XOA_PLAN < 5 && (
<div>
{this._updaterDown(trial) && (
<p className='text-danger'>{_('trialLocked')}</p>
)}
</div>
)}
</CardBlock>
</Card>
</Col>
</Row>
</Container>
)
}
}
const UpdateAlarm = () => (
<span className='fa-stack'>
<i className='fa fa-circle fa-stack-2x text-danger' />
<i className='fa fa-exclamation fa-stack-1x' />
</span>
)
const UpdateError = () => (
<span className='fa-stack'>
<i className='fa fa-circle fa-stack-2x text-danger' />
<i className='fa fa-question fa-stack-1x' />
</span>
)
const UpdateWarning = () => (
<span className='fa-stack'>
<i className='fa fa-circle fa-stack-2x text-warning' />
<i className='fa fa-question fa-stack-1x' />
</span>
)
const UpdateSuccess = () => <Icon icon='success' />
const UpdateAlert = () => (
<span className='fa-stack'>
<i className='fa fa-circle fa-stack-2x text-success' />
<i className='fa fa-bell fa-stack-1x' />
</span>
)
const RegisterAlarm = () => (
<Icon icon='not-registered' className='text-warning' />
)
export const UpdateTag = connectStore(state => {
return {
configuration: state.xoaConfiguration,
log: state.xoaUpdaterLog,
registration: state.xoaRegisterState,
state: state.xoaUpdaterState,
trial: state.xoaTrialState,
}
})(props => {
const { state } = props
const components = {
disconnected: <UpdateError />,
connected: <UpdateWarning />,
upToDate: <UpdateSuccess />,
upgradeNeeded: <UpdateAlert />,
registerNeeded: <RegisterAlarm />,
error: <UpdateAlarm />,
}
const tooltips = {
disconnected: _('noUpdateInfo'),
connected: _('waitingUpdateInfo'),
upToDate: _('upToDate'),
upgradeNeeded: _('mustUpgrade'),
registerNeeded: _('registerNeeded'),
error: _('updaterError'),
}
return <Tooltip content={tooltips[state]}>{components[state]}</Tooltip>
})

View File

@ -8,27 +8,30 @@ import React from 'react'
import SortedTable from 'sorted-table' import SortedTable from 'sorted-table'
import Tooltip from 'tooltip' import Tooltip from 'tooltip'
import { Container, Col, Row } from 'grid' import { Container, Col, Row } from 'grid'
import { get } from 'xo-defined'
import { import {
every, every,
filter, filter,
find, find,
flatten,
forEach, forEach,
isEmpty, isEmpty,
map, map,
mapValues, mapValues,
some, some,
} from 'lodash' } from 'lodash'
import { createGetObjectsOfType, createSelector } from 'selectors' import { createGetObjectsOfType, createSelector, isAdmin } from 'selectors'
import { import {
addSubscriptions, addSubscriptions,
connectStore, connectStore,
cowSet, cowSet,
formatSize, formatSize,
isXosanPack, isXosanPack,
ShortDate,
} from 'utils' } from 'utils'
import { import {
deleteSr, deleteSr,
registerXosan, getLicenses,
subscribePlugins, subscribePlugins,
subscribeResourceCatalog, subscribeResourceCatalog,
subscribeVolumeInfo, subscribeVolumeInfo,
@ -38,6 +41,7 @@ import NewXosan from './new-xosan'
import CreationProgress from './creation-progress' import CreationProgress from './creation-progress'
export const INFO_TYPES = ['heal', 'status', 'info', 'statusDetail', 'hosts'] export const INFO_TYPES = ['heal', 'status', 'info', 'statusDetail', 'hosts']
const EXPIRES_SOON_DELAY = 30 * 24 * 60 * 60 * 1000 // 1 month
// ================================================================== // ==================================================================
@ -54,19 +58,22 @@ const HEADER = (
const XOSAN_COLUMNS = [ const XOSAN_COLUMNS = [
{ {
itemRenderer: (sr, { status }) => { itemRenderer: (sr, { status }) => {
if (status === undefined || status[sr.id] === undefined) {
return null
}
const pbdsDetached = every(map(sr.pbds, 'attached')) const pbdsDetached = every(map(sr.pbds, 'attached'))
? null ? null
: _('xosanPbdsDetached') : _('xosanPbdsDetached')
const badStatus = const badStatus = every(status[sr.id])
status && every(status[sr.id]) ? null
? null : _('xosanBadStatus', {
: _('xosanBadStatus', { badStatuses: (
badStatuses: ( <ul>
<ul> {map(status, (_, status) => <li key={status}>{status}</li>)}
{map(status, (_, status) => <li key={status}>{status}</li>)} </ul>
</ul> ),
), })
})
if (pbdsDetached != null || badStatus != null) { if (pbdsDetached != null || badStatus != null) {
return ( return (
@ -131,6 +138,71 @@ const XOSAN_COLUMNS = [
) : null, ) : null,
sortCriteria: sr => sr.physical_usage * 100 / sr.size, sortCriteria: sr => sr.physical_usage * 100 / sr.size,
}, },
{
name: _('xosanLicense'),
itemRenderer: (sr, { isAdmin, licensesByXosan, licenseError }) => {
if (licenseError !== undefined) {
return
}
const license = licensesByXosan[sr.id]
// XOSAN not bound to any license, not even trial
if (license === undefined) {
return (
<span className='text-danger'>
{_('xosanUnknownSr')}{' '}
<a href='https://xen-orchestra.com/'>{_('contactUs')}</a>
</span>
)
}
// XOSAN bound to multiple licenses
if (license === null) {
return (
<span className='text-danger'>
{_('xosanMultipleLicenses')}{' '}
<a href='https://xen-orchestra.com/'>{_('contactUs')}</a>
</span>
)
}
const now = Date.now()
const expiresSoon = license.expires - now < EXPIRES_SOON_DELAY
const expired = license.expires < now
return license.productId === 'xosan' ? (
<span>
{license.expires === undefined ? (
'✔'
) : expired ? (
<span>
{_('xosanLicenseHasExpired')}{' '}
{isAdmin && (
<Link to='/xoa/licenses'>{_('xosanUpdateLicenseMessage')}</Link>
)}
</span>
) : (
<span className={expiresSoon && 'text-danger'}>
{_('xosanLicenseExpiresDate', {
date: <ShortDate timestamp={license.expires} />,
})}{' '}
{expiresSoon &&
isAdmin && (
<Link to='/xoa/licenses'>
{_('xosanUpdateLicenseMessage')}
</Link>
)}
</span>
)}
</span>
) : (
<span>
{_('xosanNoLicense')}{' '}
<Link to='/xoa/licenses'>{_('xosanUnlockNow')}</Link>
</span>
)
},
},
] ]
const XOSAN_INDIVIDUAL_ACTIONS = [ const XOSAN_INDIVIDUAL_ACTIONS = [
@ -214,11 +286,13 @@ const XOSAN_INDIVIDUAL_ACTIONS = [
) )
return { return {
isAdmin,
isMasterOfflineByPool: getIsMasterOfflineByPool, isMasterOfflineByPool: getIsMasterOfflineByPool,
hostsNeedRestartByPool: getHostsNeedRestartByPool, hostsNeedRestartByPool: getHostsNeedRestartByPool,
noPacksByPool, noPacksByPool,
poolPredicate: getPoolPredicate, poolPredicate: getPoolPredicate,
pools: getPools, pools: getPools,
xoaRegistration: state => state.xoaRegisterState,
xosanSrs: getXosanSrs, xosanSrs: getXosanSrs,
} }
}) })
@ -228,20 +302,51 @@ const XOSAN_INDIVIDUAL_ACTIONS = [
}) })
export default class Xosan extends Component { export default class Xosan extends Component {
componentDidMount () { componentDidMount () {
this._subscribeVolumeInfo(this.props.xosanSrs) this._updateLicenses().then(() =>
this._subscribeVolumeInfo(this.props.xosanSrs)
)
} }
componentWillReceiveProps ({ pools, xosanSrs }) { componentWillReceiveProps ({ pools, xosanSrs }) {
if (xosanSrs !== this.props.xosanSrs) this._subscribeVolumeInfo(xosanSrs) if (xosanSrs !== this.props.xosanSrs) {
this.unsubscribeVolumeInfo && this.unsubscribeVolumeInfo()
this._subscribeVolumeInfo(xosanSrs)
}
} }
componentWillUnmount () { componentWillUnmount () {
if (this.unsubscribeVolumeInfo != null) this.unsubscribeVolumeInfo() if (this.unsubscribeVolumeInfo != null) this.unsubscribeVolumeInfo()
} }
_updateLicenses = () =>
Promise.all([getLicenses('xosan'), getLicenses('xosan.trial')])
.then(([xosanLicenses, xosanTrialLicenses]) => {
this.setState({
xosanLicenses,
xosanTrialLicenses,
})
})
.catch(error => {
this.setState({ licenseError: error })
})
_subscribeVolumeInfo = srs => { _subscribeVolumeInfo = srs => {
const licensesByXosan = this._getLicensesByXosan()
const now = Date.now()
const canAdminXosan = sr => {
const license = licensesByXosan[sr.id]
return (
license !== undefined &&
(license.expires === undefined || license.expires > now)
)
}
const unsubscriptions = [] const unsubscriptions = []
forEach(srs, sr => { forEach(srs, sr => {
if (!canAdminXosan(sr)) {
return
}
forEach(INFO_TYPES, infoType => forEach(INFO_TYPES, infoType =>
unsubscriptions.push( unsubscriptions.push(
subscribeVolumeInfo({ sr, infoType }, info => subscribeVolumeInfo({ sr, infoType }, info =>
@ -256,10 +361,29 @@ export default class Xosan extends Component {
forEach(unsubscriptions, unsubscribe => unsubscribe()) forEach(unsubscriptions, unsubscribe => unsubscribe())
} }
_getLicensesByXosan = createSelector(
() => this.state.xosanLicenses,
() => this.state.xosanTrialLicenses,
(xosanLicenses = [], xosanTrialLicenses = []) => {
const licensesByXosan = {}
forEach(flatten([xosanLicenses, xosanTrialLicenses]), license => {
let xosanId
if ((xosanId = license.boundObjectId) === undefined) {
return
}
licensesByXosan[xosanId] =
licensesByXosan[xosanId] !== undefined
? null // XOSAN bound to multiple licenses!
: license
})
return licensesByXosan
}
)
_getError = createSelector( _getError = createSelector(
() => this.props.plugins, () => this.props.plugins,
() => this.props.catalog, plugins => {
(plugins, catalog) => {
const cloudPlugin = find(plugins, { id: 'cloud' }) const cloudPlugin = find(plugins, { id: 'cloud' })
if (!cloudPlugin) { if (!cloudPlugin) {
return _('xosanInstallCloudPlugin') return _('xosanInstallCloudPlugin')
@ -268,43 +392,33 @@ export default class Xosan extends Component {
if (!cloudPlugin.loaded) { if (!cloudPlugin.loaded) {
return _('xosanLoadCloudPlugin') return _('xosanLoadCloudPlugin')
} }
if (!catalog) {
return _('xosanLoading')
}
const { xosan } = catalog._namespaces
if (!xosan) {
return (
<span>
<Icon icon='error' /> {_('xosanNotAvailable')}
</span>
)
}
if (xosan.available) {
return (
<ActionButton handler={registerXosan} btnStyle='primary' icon='add'>
{_('xosanRegisterBeta')}
</ActionButton>
)
}
if (xosan.pending) {
return _('xosanSuccessfullyRegistered')
}
} }
) )
_showBetaIsOver = createSelector(
() => this.props.catalog,
() => this.state.xosanLicenses,
() => this.state.xosanTrialLicenses,
() => this.state.licenseError,
(catalog, xosanLicenses, xosanTrialLicenses, licenseError) =>
licenseError === undefined &&
get(() => catalog._namespaces.xosan) !== undefined &&
isEmpty(xosanLicenses) &&
isEmpty(xosanTrialLicenses)
)
_onSrCreationStarted = () => this.setState({ showNewXosanForm: false }) _onSrCreationStarted = () => this.setState({ showNewXosanForm: false })
render () { render () {
const { const {
xosanSrs,
noPacksByPool,
hostsNeedRestartByPool, hostsNeedRestartByPool,
isAdmin,
noPacksByPool,
poolPredicate, poolPredicate,
xoaRegistration,
xosanSrs,
} = this.props } = this.props
const { licenseError } = this.state
const error = this._getError() const error = this._getError()
return ( return (
@ -319,7 +433,14 @@ export default class Xosan extends Component {
</Row> </Row>
) : ( ) : (
[ [
<Row className='mb-1'> this._showBetaIsOver() && (
<Row key='beta-is-over'>
<Col>
<em>{_('xosanBetaOverMessage')}</em>
</Col>
</Row>
),
<Row key='new-button' className='mb-1'>
<Col> <Col>
<ActionButton <ActionButton
btnStyle='primary' btnStyle='primary'
@ -330,26 +451,39 @@ export default class Xosan extends Component {
</ActionButton> </ActionButton>
</Col> </Col>
</Row>, </Row>,
<Row> <Row key='new-form'>
<Col> <Col>
{this.state.showNewXosanForm && ( {this.state.showNewXosanForm && (
<NewXosan <NewXosan
hostsNeedRestartByPool={hostsNeedRestartByPool} hostsNeedRestartByPool={hostsNeedRestartByPool}
noPacksByPool={noPacksByPool} noPacksByPool={noPacksByPool}
poolPredicate={poolPredicate} poolPredicate={poolPredicate}
onSrCreationFinished={this._updateLicenses}
onSrCreationStarted={this._onSrCreationStarted} onSrCreationStarted={this._onSrCreationStarted}
notRegistered={
get(() => xoaRegistration.state) !== 'registered'
}
/> />
)} )}
</Col> </Col>
</Row>, </Row>,
<Row> <Row key='progress'>
<Col> <Col>
{map(this.props.pools, pool => ( {map(this.props.pools, pool => (
<CreationProgress key={pool.id} pool={pool} /> <CreationProgress key={pool.id} pool={pool} />
))} ))}
</Col> </Col>
</Row>, </Row>,
<Row> licenseError !== undefined && (
<Row>
<Col>
<em className='text-danger'>
{_('xosanGetLicensesError')}
</em>
</Col>
</Row>
),
<Row key='srs'>
<Col> <Col>
{isEmpty(xosanSrs) ? ( {isEmpty(xosanSrs) ? (
<em>{_('xosanNoSrs')}</em> <em>{_('xosanNoSrs')}</em>
@ -359,6 +493,9 @@ export default class Xosan extends Component {
columns={XOSAN_COLUMNS} columns={XOSAN_COLUMNS}
individualActions={XOSAN_INDIVIDUAL_ACTIONS} individualActions={XOSAN_INDIVIDUAL_ACTIONS}
userData={{ userData={{
isAdmin,
licensesByXosan: this._getLicensesByXosan(),
licenseError,
status: this.state.status, status: this.state.status,
}} }}
/> />

View File

@ -222,12 +222,24 @@ export default class NewXosan extends Component {
brickSize: this.state.customBrickSize ? this.state.brickSize : undefined, brickSize: this.state.customBrickSize ? this.state.brickSize : undefined,
memorySize: this.state.memorySize, memorySize: this.state.memorySize,
ipRange: this.state.customIpRange ? this.state.ipRange : undefined, ipRange: this.state.customIpRange ? this.state.ipRange : undefined,
}) }).then(this.props.onSrCreationFinished)
this.props.onSrCreationStarted() this.props.onSrCreationStarted()
} }
render () { render () {
if (process.env.XOA_PLAN === 5) {
return (
<em>
{_('xosanSourcesDisclaimer', {
link: (
<a href='https://xen-orchestra.com'>https://xen-orchestra.com</a>
),
})}
</em>
)
}
const { const {
brickSize, brickSize,
customBrickSize, customBrickSize,
@ -243,7 +255,22 @@ export default class NewXosan extends Component {
vlan, vlan,
} = this.state } = this.state
const { hostsNeedRestartByPool, noPacksByPool, poolPredicate } = this.props const {
hostsNeedRestartByPool,
noPacksByPool,
poolPredicate,
notRegistered,
} = this.props
if (notRegistered) {
return (
<em>
{_('xosanUnregisteredDisclaimer', {
link: <Link to='/xoa/update'>{_('registerNow')}</Link>,
})}
</em>
)
}
const lvmsrs = this._getLvmSrs() const lvmsrs = this._getLvmSrs()
const hosts = this._getHosts() const hosts = this._getHosts()
@ -253,6 +280,7 @@ export default class NewXosan extends Component {
pool !== undefined && pool !== undefined &&
hostsNeedRestartByPool !== undefined && hostsNeedRestartByPool !== undefined &&
hostsNeedRestartByPool[pool.id] hostsNeedRestartByPool[pool.id]
const architecture = suggestions !== undefined && suggestions[suggestion]
return ( return (
<Container> <Container>
@ -416,11 +444,22 @@ export default class NewXosan extends Component {
)} )}
</tbody> </tbody>
</table> </table>
{architecture.layout === 'disperse' && (
<div className='alert alert-danger'>
{_('xosanDisperseWarning', {
link: (
<a href='https://xen-orchestra.com/docs/xosan_types.html'>
xen-orchestra.com/docs/xosan_types.html
</a>
),
})}
</div>
)}
<Graph <Graph
height={160} height={160}
layout={suggestions[suggestion].layout} layout={architecture.layout}
nSrs={this._getNSelectedSrs()} nSrs={this._getNSelectedSrs()}
redundancy={suggestions[suggestion].redundancy} redundancy={architecture.redundancy}
width={600} width={600}
/> />
<hr /> <hr />