feat(xosan): license management (#2528)
This commit is contained in:
parent
8cb53b0c4e
commit
02c715e1cc
@ -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)) {
|
||||||
|
@ -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' />
|
||||||
|
)
|
||||||
|
@ -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 })
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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 => {
|
||||||
|
@ -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',
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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
58
src/xo-app/xoa/index.js
Normal 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
|
234
src/xo-app/xoa/licenses/index.js
Normal file
234
src/xo-app/xoa/licenses/index.js
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
192
src/xo-app/xoa/licenses/xosan.js
Normal file
192
src/xo-app/xoa/licenses/xosan.js
Normal 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,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
468
src/xo-app/xoa/update/index.js
Normal file
468
src/xo-app/xoa/update/index.js
Normal 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>
|
||||||
|
})
|
@ -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,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -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 />
|
||||||
|
Loading…
Reference in New Issue
Block a user