feat(xosan): license management (#2528)
This commit is contained in:
parent
8cb53b0c4e
commit
02c715e1cc
@ -57,7 +57,9 @@ const messages = {
|
||||
selfServicePage: 'Self service',
|
||||
backupPage: 'Backup',
|
||||
jobsPage: 'Jobs',
|
||||
xoaPage: 'XOA',
|
||||
updatePage: 'Updates',
|
||||
licensesPage: 'Licenses',
|
||||
settingsPage: 'Settings',
|
||||
settingsServersPage: 'Servers',
|
||||
settingsUsersPage: 'Users',
|
||||
@ -95,6 +97,7 @@ const messages = {
|
||||
jobsSchedulingPage: 'Scheduling',
|
||||
customJob: 'Custom Job',
|
||||
userPage: 'User',
|
||||
xoa: 'XOA',
|
||||
|
||||
// ----- Support -----
|
||||
noSupport: 'No support',
|
||||
@ -1547,6 +1550,8 @@ const messages = {
|
||||
xosanSrTitle: 'Xen Orchestra SAN SR',
|
||||
xosanAvailableSrsTitle: 'Select local SRs (lvm)',
|
||||
xosanSuggestions: 'Suggestions',
|
||||
xosanDisperseWarning:
|
||||
'Warning: using disperse layout is not recommended right now. Please read {link}.',
|
||||
xosanName: 'Name',
|
||||
xosanHost: 'Host',
|
||||
xosanHosts: 'Connected Hosts',
|
||||
@ -1554,6 +1559,8 @@ const messages = {
|
||||
xosanVolumeId: 'Volume ID',
|
||||
xosanSize: 'Size',
|
||||
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.',
|
||||
xosanInstallIt: 'Install it now!',
|
||||
xosanNeedRestart:
|
||||
@ -1595,11 +1602,9 @@ const messages = {
|
||||
// Pack download modal
|
||||
xosanInstallCloudPlugin: 'Install cloud plugin first',
|
||||
xosanLoadCloudPlugin: 'Load cloud plugin first',
|
||||
xosanRegister: 'Register your appliance first',
|
||||
xosanLoading: 'Loading…',
|
||||
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:',
|
||||
xosanInstallPack: 'Install {pack} v{version}?',
|
||||
xosanNoPackFound:
|
||||
@ -1641,11 +1646,53 @@ const messages = {
|
||||
xosanRemove: 'Remove',
|
||||
xosanVolume: 'Volume',
|
||||
xosanVolumeOptions: 'Volume options',
|
||||
xosanCouldNotFindVM: 'Could not find VM',
|
||||
xosanCouldNotFindVm: 'Could not find VM',
|
||||
xosanUnderlyingStorageUsage: 'Using {usage}',
|
||||
xosanCustomIpNetwork: 'Custom IP network (/24)',
|
||||
xosanIssueHostNotInNetwork:
|
||||
'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) {
|
||||
if (isString(message)) {
|
||||
|
@ -3,6 +3,7 @@ import humanFormat from 'human-format'
|
||||
import React from 'react'
|
||||
import ReadableStream from 'readable-stream'
|
||||
import { connect } from 'react-redux'
|
||||
import { FormattedDate } from 'react-intl'
|
||||
import {
|
||||
clone,
|
||||
escapeRegExp,
|
||||
@ -572,6 +573,9 @@ export const cowSet = (object, path, value, depth = 0) => {
|
||||
// This function returns an estimated progress value between 0 and 1
|
||||
// based on the elapsed time since the createFakeProgress call and
|
||||
// the given estimated duration d
|
||||
//
|
||||
// const getProgress = createFakeProgress(120)
|
||||
// setInterval(() => console.log(`Progress: ${getProgress() * 100} %`), 1000)
|
||||
export const createFakeProgress = (() => {
|
||||
const S = 0.95 // Progress value after d seconds
|
||||
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)
|
||||
}
|
||||
subscribeCheckSrCurrentState.forceRefresh = pool => {
|
||||
if (pool === undefined) {
|
||||
forEach(checkSrCurrentStateSubscriptions, subscription =>
|
||||
subscription.forceRefresh()
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const subscription = checkSrCurrentStateSubscriptions[resolveId(pool)]
|
||||
if (subscription !== undefined) {
|
||||
subscription.forceRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
const missingPatchesByHost = {}
|
||||
export const subscribeHostMissingPatches = (host, cb) => {
|
||||
@ -2017,8 +2030,8 @@ export const createXosanSR = ({
|
||||
brickSize,
|
||||
memorySize,
|
||||
ipRange,
|
||||
}) =>
|
||||
_call('xosan.createSR', {
|
||||
}) => {
|
||||
const promise = _call('xosan.createSR', {
|
||||
template,
|
||||
pif: pif.id,
|
||||
vlan: String(vlan),
|
||||
@ -2030,6 +2043,12 @@ export const createXosanSR = ({
|
||||
ipRange,
|
||||
})
|
||||
|
||||
// Force refresh in parallel to get the creation progress sooner
|
||||
subscribeCheckSrCurrentState.forceRefresh()
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
export const addXosanBricks = (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) =>
|
||||
_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;
|
||||
}
|
||||
}
|
||||
&-menu-xoa {
|
||||
@extend .fa;
|
||||
@extend .fa-cube;
|
||||
}
|
||||
&-menu-update {
|
||||
@extend .fa;
|
||||
@extend .fa-refresh;
|
||||
}
|
||||
&-menu-license {
|
||||
@extend .fa;
|
||||
@extend .fa-file-text-o;
|
||||
}
|
||||
&-menu-settings {
|
||||
@extend .fa;
|
||||
@extend .fa-cog;
|
||||
@ -985,6 +993,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-star;
|
||||
}
|
||||
&-support {
|
||||
@extend .fa;
|
||||
@extend .fa-support;
|
||||
}
|
||||
|
||||
// XOSAN related
|
||||
|
||||
|
@ -100,7 +100,7 @@ export default class About extends Component {
|
||||
<div>
|
||||
<Row>
|
||||
<Col>
|
||||
<Link to={'/xoa-update'}>
|
||||
<Link to='/xoa-update'>
|
||||
<h2>{_('freeTrial')}</h2>
|
||||
{_('freeTrialNow')}
|
||||
</Link>
|
||||
|
@ -38,7 +38,8 @@ import Tasks from './tasks'
|
||||
import User from './user'
|
||||
import Vm from './vm'
|
||||
import VmImport from './vm-import'
|
||||
import XoaUpdates from './xoa-updates'
|
||||
import Xoa from './xoa'
|
||||
import XoaUpdates from './xoa/update'
|
||||
import Xosan from './xosan'
|
||||
|
||||
import keymap, { help } from '../keymap'
|
||||
@ -87,7 +88,7 @@ const BODY_STYLE = {
|
||||
'vms/import': VmImport,
|
||||
'vms/new': NewVm,
|
||||
'vms/:id': Vm,
|
||||
'xoa-update': XoaUpdates,
|
||||
xoa: Xoa,
|
||||
xosan: Xosan,
|
||||
})
|
||||
@connectStore(state => {
|
||||
|
@ -7,7 +7,7 @@ import Link from 'link'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import { UpdateTag } from '../xoa-updates'
|
||||
import { UpdateTag } from '../xoa/update'
|
||||
import { addSubscriptions, connectStore, getXoaPlan, noop } from 'utils'
|
||||
import {
|
||||
connect,
|
||||
@ -203,10 +203,14 @@ export default class Menu extends Component {
|
||||
],
|
||||
},
|
||||
isAdmin && {
|
||||
to: '/xoa-update',
|
||||
icon: 'menu-update',
|
||||
label: 'updatePage',
|
||||
to: 'xoa/update',
|
||||
icon: 'menu-xoa',
|
||||
label: 'xoa',
|
||||
extra: <UpdateTag />,
|
||||
subMenu: [
|
||||
{ to: 'xoa/update', icon: 'menu-update', label: 'updatePage' },
|
||||
{ to: 'xoa/licenses', icon: 'menu-license', label: 'licensesPage' },
|
||||
],
|
||||
},
|
||||
isAdmin && {
|
||||
to: '/settings/servers',
|
||||
|
@ -14,10 +14,15 @@ export default class ReplaceBrickModalBody extends Component {
|
||||
_getSrPredicate = createSelector(
|
||||
() => this.props.vm,
|
||||
() => this.state.onSameVm,
|
||||
(vm, onSameVm) =>
|
||||
onSameVm
|
||||
(vm, onSameVm) => {
|
||||
if (vm === undefined) {
|
||||
return sr => sr.SR_type === 'lvm'
|
||||
}
|
||||
|
||||
return onSameVm
|
||||
? sr => sr.$container === vm.$container && sr.SR_type === 'lvm'
|
||||
: sr => sr.$pool === vm.$pool && sr.SR_type === 'lvm'
|
||||
}
|
||||
)
|
||||
|
||||
_toggleOnSameVm = () =>
|
||||
@ -36,6 +41,7 @@ export default class ReplaceBrickModalBody extends Component {
|
||||
render () {
|
||||
return (
|
||||
<Container>
|
||||
{this.props.vm !== undefined && (
|
||||
<Row className='mb-1'>
|
||||
<Col size={6}>
|
||||
<strong>{_('xosanOnSameVm')}</strong>
|
||||
@ -47,6 +53,7 @@ export default class ReplaceBrickModalBody extends Component {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
<Row className='mb-1'>
|
||||
<Col size={6}>
|
||||
<strong>{_('xosanUnderlyingStorage')}</strong>
|
||||
|
@ -1,10 +1,11 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import HomeTags from 'home-tags'
|
||||
import Icon from 'icon'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import Usage, { UsageElement } from 'usage'
|
||||
import { addTag, removeTag } from 'xo'
|
||||
import { addTag, removeTag, getLicense } from 'xo'
|
||||
import { connectStore, formatSize } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createGetObject } from 'selectors'
|
||||
@ -20,7 +21,21 @@ const UsageTooltip = connectStore(() => ({
|
||||
</span>
|
||||
))
|
||||
|
||||
export default ({ sr, vdis, vdiSnapshots, unmanagedVdis }) => (
|
||||
export default class TabGeneral extends Component {
|
||||
componentDidMount () {
|
||||
const { sr } = this.props
|
||||
|
||||
if (sr.SR_type === 'xosan') {
|
||||
getLicense('xosan.trial', sr.id).then(() =>
|
||||
this.setState({ licenseRestriction: true })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { sr, vdis, vdiSnapshots, unmanagedVdis } = this.props
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Row className='text-xs-center'>
|
||||
<Col mediumSize={4}>
|
||||
@ -33,6 +48,9 @@ export default ({ sr, vdis, vdiSnapshots, unmanagedVdis }) => (
|
||||
{formatSize(sr.size)} <Icon icon='sr' size='lg' />
|
||||
</h2>
|
||||
<p>Type: {sr.SR_type}</p>
|
||||
{this.state.licenseRestriction && (
|
||||
<p className='text-danger'>{_('xosanLicenseRestricted')}</p>
|
||||
)}
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
<h2>
|
||||
@ -92,4 +110,6 @@ export default ({ sr, vdis, vdiSnapshots, unmanagedVdis }) => (
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -12,16 +12,18 @@ import { confirm } from 'modal'
|
||||
import { error } from 'notification'
|
||||
import { Toggle } from 'form'
|
||||
import { Container, Col, Row } from 'grid'
|
||||
import { forEach, isEmpty, map, reduce, sum } from 'lodash'
|
||||
import { createGetObjectsOfType, createSelector } from 'selectors'
|
||||
import { find, forEach, isEmpty, map, reduce, sum } from 'lodash'
|
||||
import { createGetObjectsOfType, createSelector, isAdmin } from 'selectors'
|
||||
import { addSubscriptions, connectStore, formatSize } from 'utils'
|
||||
import {
|
||||
addXosanBricks,
|
||||
getLicense,
|
||||
fixHostNotInXosanNetwork,
|
||||
// TODO: uncomment when implementing subvolume deletion
|
||||
// removeXosanBricks,
|
||||
replaceXosanBrick,
|
||||
startVm,
|
||||
subscribePlugins,
|
||||
subscribeVolumeInfo,
|
||||
} from 'xo'
|
||||
|
||||
@ -350,6 +352,7 @@ class Node extends Component {
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@connectStore(() => ({
|
||||
isAdmin,
|
||||
vms: createGetObjectsOfType('VM'),
|
||||
hosts: createGetObjectsOfType('host'),
|
||||
vbds: createGetObjectsOfType('VBD'),
|
||||
@ -362,9 +365,22 @@ class Node extends Component {
|
||||
subscribeVolumeInfo({ sr, infoType }, cb)
|
||||
})
|
||||
|
||||
subscriptions.plugins = subscribePlugins
|
||||
|
||||
return subscriptions
|
||||
})
|
||||
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 () => {
|
||||
const { srs, brickSize } = await confirm({
|
||||
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(
|
||||
() => this.props.sr && this.props.sr.other_config['xo:xosan_config'],
|
||||
otherConfig => (otherConfig ? JSON.parse(otherConfig) : null)
|
||||
@ -582,13 +616,59 @@ export default class TabXosan extends Component {
|
||||
)
|
||||
|
||||
render () {
|
||||
const { showAdvanced } = this.state
|
||||
const { heal_, info_, sr, status_, statusDetail_, vbds, vdis } = this.props
|
||||
const { license, licenseError, showAdvanced } = this.state
|
||||
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()
|
||||
if (
|
||||
(license === undefined && licenseError === undefined) ||
|
||||
xosanConfig === undefined
|
||||
) {
|
||||
return <em>{_('statusLoading')}</em>
|
||||
}
|
||||
|
||||
if (!xosanConfig) {
|
||||
return null
|
||||
if (
|
||||
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) {
|
||||
|
@ -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 Tooltip from 'tooltip'
|
||||
import { Container, Col, Row } from 'grid'
|
||||
import { get } from 'xo-defined'
|
||||
import {
|
||||
every,
|
||||
filter,
|
||||
find,
|
||||
flatten,
|
||||
forEach,
|
||||
isEmpty,
|
||||
map,
|
||||
mapValues,
|
||||
some,
|
||||
} from 'lodash'
|
||||
import { createGetObjectsOfType, createSelector } from 'selectors'
|
||||
import { createGetObjectsOfType, createSelector, isAdmin } from 'selectors'
|
||||
import {
|
||||
addSubscriptions,
|
||||
connectStore,
|
||||
cowSet,
|
||||
formatSize,
|
||||
isXosanPack,
|
||||
ShortDate,
|
||||
} from 'utils'
|
||||
import {
|
||||
deleteSr,
|
||||
registerXosan,
|
||||
getLicenses,
|
||||
subscribePlugins,
|
||||
subscribeResourceCatalog,
|
||||
subscribeVolumeInfo,
|
||||
@ -38,6 +41,7 @@ import NewXosan from './new-xosan'
|
||||
import CreationProgress from './creation-progress'
|
||||
|
||||
export const INFO_TYPES = ['heal', 'status', 'info', 'statusDetail', 'hosts']
|
||||
const EXPIRES_SOON_DELAY = 30 * 24 * 60 * 60 * 1000 // 1 month
|
||||
|
||||
// ==================================================================
|
||||
|
||||
@ -54,11 +58,14 @@ const HEADER = (
|
||||
const XOSAN_COLUMNS = [
|
||||
{
|
||||
itemRenderer: (sr, { status }) => {
|
||||
if (status === undefined || status[sr.id] === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
const pbdsDetached = every(map(sr.pbds, 'attached'))
|
||||
? null
|
||||
: _('xosanPbdsDetached')
|
||||
const badStatus =
|
||||
status && every(status[sr.id])
|
||||
const badStatus = every(status[sr.id])
|
||||
? null
|
||||
: _('xosanBadStatus', {
|
||||
badStatuses: (
|
||||
@ -131,6 +138,71 @@ const XOSAN_COLUMNS = [
|
||||
) : null,
|
||||
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 = [
|
||||
@ -214,11 +286,13 @@ const XOSAN_INDIVIDUAL_ACTIONS = [
|
||||
)
|
||||
|
||||
return {
|
||||
isAdmin,
|
||||
isMasterOfflineByPool: getIsMasterOfflineByPool,
|
||||
hostsNeedRestartByPool: getHostsNeedRestartByPool,
|
||||
noPacksByPool,
|
||||
poolPredicate: getPoolPredicate,
|
||||
pools: getPools,
|
||||
xoaRegistration: state => state.xoaRegisterState,
|
||||
xosanSrs: getXosanSrs,
|
||||
}
|
||||
})
|
||||
@ -228,20 +302,51 @@ const XOSAN_INDIVIDUAL_ACTIONS = [
|
||||
})
|
||||
export default class Xosan extends Component {
|
||||
componentDidMount () {
|
||||
this._updateLicenses().then(() =>
|
||||
this._subscribeVolumeInfo(this.props.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 () {
|
||||
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 => {
|
||||
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 = []
|
||||
forEach(srs, sr => {
|
||||
if (!canAdminXosan(sr)) {
|
||||
return
|
||||
}
|
||||
forEach(INFO_TYPES, infoType =>
|
||||
unsubscriptions.push(
|
||||
subscribeVolumeInfo({ sr, infoType }, info =>
|
||||
@ -256,10 +361,29 @@ export default class Xosan extends Component {
|
||||
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(
|
||||
() => this.props.plugins,
|
||||
() => this.props.catalog,
|
||||
(plugins, catalog) => {
|
||||
plugins => {
|
||||
const cloudPlugin = find(plugins, { id: 'cloud' })
|
||||
if (!cloudPlugin) {
|
||||
return _('xosanInstallCloudPlugin')
|
||||
@ -268,43 +392,33 @@ export default class Xosan extends Component {
|
||||
if (!cloudPlugin.loaded) {
|
||||
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 })
|
||||
|
||||
render () {
|
||||
const {
|
||||
xosanSrs,
|
||||
noPacksByPool,
|
||||
hostsNeedRestartByPool,
|
||||
isAdmin,
|
||||
noPacksByPool,
|
||||
poolPredicate,
|
||||
xoaRegistration,
|
||||
xosanSrs,
|
||||
} = this.props
|
||||
const { licenseError } = this.state
|
||||
const error = this._getError()
|
||||
|
||||
return (
|
||||
@ -319,7 +433,14 @@ export default class Xosan extends Component {
|
||||
</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>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
@ -330,26 +451,39 @@ export default class Xosan extends Component {
|
||||
</ActionButton>
|
||||
</Col>
|
||||
</Row>,
|
||||
<Row>
|
||||
<Row key='new-form'>
|
||||
<Col>
|
||||
{this.state.showNewXosanForm && (
|
||||
<NewXosan
|
||||
hostsNeedRestartByPool={hostsNeedRestartByPool}
|
||||
noPacksByPool={noPacksByPool}
|
||||
poolPredicate={poolPredicate}
|
||||
onSrCreationFinished={this._updateLicenses}
|
||||
onSrCreationStarted={this._onSrCreationStarted}
|
||||
notRegistered={
|
||||
get(() => xoaRegistration.state) !== 'registered'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
</Row>,
|
||||
<Row>
|
||||
<Row key='progress'>
|
||||
<Col>
|
||||
{map(this.props.pools, pool => (
|
||||
<CreationProgress key={pool.id} pool={pool} />
|
||||
))}
|
||||
</Col>
|
||||
</Row>,
|
||||
licenseError !== undefined && (
|
||||
<Row>
|
||||
<Col>
|
||||
<em className='text-danger'>
|
||||
{_('xosanGetLicensesError')}
|
||||
</em>
|
||||
</Col>
|
||||
</Row>
|
||||
),
|
||||
<Row key='srs'>
|
||||
<Col>
|
||||
{isEmpty(xosanSrs) ? (
|
||||
<em>{_('xosanNoSrs')}</em>
|
||||
@ -359,6 +493,9 @@ export default class Xosan extends Component {
|
||||
columns={XOSAN_COLUMNS}
|
||||
individualActions={XOSAN_INDIVIDUAL_ACTIONS}
|
||||
userData={{
|
||||
isAdmin,
|
||||
licensesByXosan: this._getLicensesByXosan(),
|
||||
licenseError,
|
||||
status: this.state.status,
|
||||
}}
|
||||
/>
|
||||
|
@ -222,12 +222,24 @@ export default class NewXosan extends Component {
|
||||
brickSize: this.state.customBrickSize ? this.state.brickSize : undefined,
|
||||
memorySize: this.state.memorySize,
|
||||
ipRange: this.state.customIpRange ? this.state.ipRange : undefined,
|
||||
})
|
||||
}).then(this.props.onSrCreationFinished)
|
||||
|
||||
this.props.onSrCreationStarted()
|
||||
}
|
||||
|
||||
render () {
|
||||
if (process.env.XOA_PLAN === 5) {
|
||||
return (
|
||||
<em>
|
||||
{_('xosanSourcesDisclaimer', {
|
||||
link: (
|
||||
<a href='https://xen-orchestra.com'>https://xen-orchestra.com</a>
|
||||
),
|
||||
})}
|
||||
</em>
|
||||
)
|
||||
}
|
||||
|
||||
const {
|
||||
brickSize,
|
||||
customBrickSize,
|
||||
@ -243,7 +255,22 @@ export default class NewXosan extends Component {
|
||||
vlan,
|
||||
} = 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 hosts = this._getHosts()
|
||||
@ -253,6 +280,7 @@ export default class NewXosan extends Component {
|
||||
pool !== undefined &&
|
||||
hostsNeedRestartByPool !== undefined &&
|
||||
hostsNeedRestartByPool[pool.id]
|
||||
const architecture = suggestions !== undefined && suggestions[suggestion]
|
||||
|
||||
return (
|
||||
<Container>
|
||||
@ -416,11 +444,22 @@ export default class NewXosan extends Component {
|
||||
)}
|
||||
</tbody>
|
||||
</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
|
||||
height={160}
|
||||
layout={suggestions[suggestion].layout}
|
||||
layout={architecture.layout}
|
||||
nSrs={this._getNSelectedSrs()}
|
||||
redundancy={suggestions[suggestion].redundancy}
|
||||
redundancy={architecture.redundancy}
|
||||
width={600}
|
||||
/>
|
||||
<hr />
|
||||
|
Loading…
Reference in New Issue
Block a user