Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
991fbaec86 | ||
|
|
fb399278b3 | ||
|
|
b868092365 | ||
|
|
80fdc6849f | ||
|
|
25ffcb952b | ||
|
|
083ac1e2d6 | ||
|
|
5a4b553a60 | ||
|
|
b1135ef566 | ||
|
|
1928d1e00f | ||
|
|
a369f7f387 | ||
|
|
33d9801dfe | ||
|
|
8c7a031cca | ||
|
|
9484d87e76 | ||
|
|
4b6822d6e5 | ||
|
|
7241a0529b | ||
|
|
66083b4e50 | ||
|
|
f631b3cc64 | ||
|
|
bb58d9b4d6 | ||
|
|
93ebff1055 | ||
|
|
08aec1c09a | ||
|
|
8ca98a56fe | ||
|
|
705f53e3e5 | ||
|
|
adaf069d20 | ||
|
|
d7be7d8660 | ||
|
|
faddee86b6 | ||
|
|
c4fcc65d16 | ||
|
|
890631d33b | ||
|
|
8e8145bb48 | ||
|
|
d73d6719a5 | ||
|
|
3419bee198 | ||
|
|
4368fad393 | ||
|
|
ab93fdbf10 | ||
|
|
8fd7697a45 | ||
|
|
1121a60912 | ||
|
|
e7b4bd2fe4 | ||
|
|
fcd8bdd1b3 | ||
|
|
e6f140f575 | ||
|
|
bfe4c45fcf | ||
|
|
f95370124b | ||
|
|
2564343816 | ||
|
|
03734eb761 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-web",
|
||||
"version": "5.7.0",
|
||||
"version": "5.7.8",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
|
||||
@@ -17,7 +17,7 @@ const cowSet = (object, path, value, depth) => {
|
||||
return value
|
||||
}
|
||||
|
||||
object = clone(object)
|
||||
object = object != null ? clone(object) : {}
|
||||
const prop = path[depth]
|
||||
object[prop] = cowSet(object[prop], path, value, depth + 1)
|
||||
return object
|
||||
|
||||
@@ -25,6 +25,8 @@ export default class Toggle extends Component {
|
||||
iconSize: 2
|
||||
}
|
||||
|
||||
_onChange = event => this.props.onChange(event.target.checked)
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
|
||||
@@ -43,7 +45,7 @@ export default class Toggle extends Component {
|
||||
checked={props.value || false}
|
||||
className={styles.checkbox}
|
||||
disabled={props.disabled}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
type='checkbox'
|
||||
/>
|
||||
</label>
|
||||
|
||||
3139
src/common/intl/locales/pl.js
Normal file
3139
src/common/intl/locales/pl.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -245,7 +245,7 @@ var messages = {
|
||||
noJobs: 'No jobs found.',
|
||||
noSchedules: 'No schedules found',
|
||||
jobActionPlaceHolder: 'Select a xo-server API command',
|
||||
jobTimeoutPlaceHolder: ' Job timeout (seconds)',
|
||||
jobTimeoutPlaceHolder: ' Timeout (number of seconds after which the job is considered failed)',
|
||||
jobSchedules: 'Schedules',
|
||||
jobScheduleNamePlaceHolder: 'Name of your schedule',
|
||||
jobScheduleJobPlaceHolder: 'Select a Job',
|
||||
@@ -554,6 +554,7 @@ var messages = {
|
||||
pifStatusDisconnected: 'Disconnected',
|
||||
pifNoInterface: 'No physical interface detected',
|
||||
pifInUse: 'This interface is currently in use',
|
||||
pifAction: 'Action',
|
||||
defaultLockingMode: 'Default locking mode',
|
||||
pifConfigureIp: 'Configure IP address',
|
||||
configIpErrorTitle: 'Invalid parameters',
|
||||
@@ -693,6 +694,8 @@ var messages = {
|
||||
vdbCreate: 'Create',
|
||||
vdbNamePlaceHolder: 'Disk name',
|
||||
vdbSizePlaceHolder: 'Size',
|
||||
cdDriveNotInstalled: 'CD drive not completely installed',
|
||||
cdDriveInstallation: 'Stop and start the VM to install the CD drive',
|
||||
saveBootOption: 'Save',
|
||||
resetBootOption: 'Reset',
|
||||
|
||||
@@ -1213,6 +1216,10 @@ var messages = {
|
||||
disconnectPifConfirm: 'Are you sure you want to disconnect this PIF?',
|
||||
deletePif: 'Delete PIF',
|
||||
deletePifConfirm: 'Are you sure you want to delete this PIF?',
|
||||
pifConnected: 'Connected',
|
||||
pifDisconnected: 'Disconnected',
|
||||
pifPhysicallyConnected: 'Physically connected',
|
||||
pifPhysicallyDisconnected: 'Physically disconnected',
|
||||
|
||||
// ----- User -----
|
||||
username: 'Username',
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import React from 'react'
|
||||
|
||||
import _ from 'intl'
|
||||
import ActionButton from './action-button'
|
||||
import Component from './base-component'
|
||||
import Icon from 'icon'
|
||||
import propTypes from './prop-types'
|
||||
import Tooltip from 'tooltip'
|
||||
import { alert } from 'modal'
|
||||
import { connectStore } from './utils'
|
||||
import { SelectVdi } from './select-objects'
|
||||
import {
|
||||
@@ -69,8 +73,10 @@ export default class IsoDevice extends Component {
|
||||
|
||||
_handleEject = () => ejectCd(this.props.vm)
|
||||
|
||||
_showWarning = () => alert(_('cdDriveNotInstalled'), _('cdDriveInstallation'))
|
||||
|
||||
render () {
|
||||
const { mountedIso } = this.props
|
||||
const {cdDrive, mountedIso} = this.props
|
||||
|
||||
return (
|
||||
<div className='input-group'>
|
||||
@@ -87,6 +93,19 @@ export default class IsoDevice extends Component {
|
||||
icon='vm-eject'
|
||||
/>
|
||||
</span>
|
||||
{mountedIso && !cdDrive.device &&
|
||||
<Tooltip content={_('cdDriveNotInstalled')}>
|
||||
<a
|
||||
className='text-warning btn btn-link'
|
||||
onClick={this._showWarning}
|
||||
>
|
||||
<Icon
|
||||
icon='alarm'
|
||||
size='lg'
|
||||
/>
|
||||
</a>
|
||||
</Tooltip>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
5
src/common/react-novnc.js
vendored
5
src/common/react-novnc.js
vendored
@@ -77,6 +77,11 @@ export default class NoVnc extends Component {
|
||||
_connect = () => {
|
||||
this._clean()
|
||||
|
||||
const { canvas } = this.refs
|
||||
if (!canvas) {
|
||||
return
|
||||
}
|
||||
|
||||
const url = parseRelativeUrl(this.props.url)
|
||||
fixProtocol(url)
|
||||
|
||||
|
||||
@@ -182,6 +182,10 @@ const renderXoItem = (item, {
|
||||
} = {}) => {
|
||||
const { id, type, label } = item
|
||||
|
||||
if (item.removed) {
|
||||
return <span key={id} className='text-danger'> <Icon icon='alarm' /> {id}</span>
|
||||
}
|
||||
|
||||
if (!type) {
|
||||
if (process.env.NODE_ENV !== 'production' && !label) {
|
||||
throw new Error(`an item must have at least either a type or a label`)
|
||||
|
||||
@@ -259,7 +259,11 @@ class TableSelect extends Component {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className='btn btn-secondary pull-right' onClick={this._reset}>
|
||||
<button
|
||||
className='btn btn-secondary pull-right'
|
||||
onClick={this._reset}
|
||||
type='button'
|
||||
>
|
||||
{_(`selectTableAll${labelId}`)} {value && !value.length && <Icon icon='success' />}
|
||||
</button>
|
||||
</div>
|
||||
@@ -447,23 +451,27 @@ class DayPicker extends Component {
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
cronPattern: propTypes.string.isRequired,
|
||||
cronPattern: propTypes.string,
|
||||
onChange: propTypes.func,
|
||||
timezone: propTypes.string
|
||||
timezone: propTypes.string,
|
||||
value: propTypes.shape({
|
||||
cronPattern: propTypes.string.isRequired,
|
||||
timezone: propTypes.string
|
||||
})
|
||||
})
|
||||
export default class Scheduler extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this._onCronChange = newCrons => {
|
||||
const cronPattern = this.props.cronPattern.split(' ')
|
||||
const cronPattern = this._getCronPattern().split(' ')
|
||||
forEach(newCrons, (cron, unit) => {
|
||||
cronPattern[PICKTIME_TO_ID[unit]] = cron
|
||||
})
|
||||
|
||||
this.props.onChange({
|
||||
cronPattern: cronPattern.join(' '),
|
||||
timezone: this.props.timezone
|
||||
timezone: this._getTimezone()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -475,17 +483,24 @@ export default class Scheduler extends Component {
|
||||
|
||||
_onTimezoneChange = timezone => {
|
||||
this.props.onChange({
|
||||
cronPattern: this.props.cronPattern,
|
||||
cronPattern: this._getCronPattern(),
|
||||
timezone
|
||||
})
|
||||
}
|
||||
|
||||
_getCronPattern = () => {
|
||||
const { value, cronPattern = value.cronPattern } = this.props
|
||||
return cronPattern
|
||||
}
|
||||
|
||||
_getTimezone = () => {
|
||||
const { value, timezone = value && value.timezone } = this.props
|
||||
return timezone
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
cronPattern,
|
||||
timezone
|
||||
} = this.props
|
||||
const cronPatternArr = cronPattern.split(' ')
|
||||
const cronPatternArr = this._getCronPattern().split(' ')
|
||||
const timezone = this._getTimezone()
|
||||
|
||||
return (
|
||||
<div className='card-block'>
|
||||
|
||||
@@ -42,7 +42,6 @@ import {
|
||||
import {
|
||||
addSubscriptions,
|
||||
connectStore,
|
||||
mapPlus,
|
||||
resolveResourceSets
|
||||
} from './utils'
|
||||
import {
|
||||
@@ -135,37 +134,6 @@ const options = props => ({
|
||||
]).isRequired
|
||||
})
|
||||
export class GenericSelect extends Component {
|
||||
componentDidUpdate (prevProps) {
|
||||
const { onChange, xoObjects } = this.props
|
||||
|
||||
if (!onChange || prevProps.xoObjects === xoObjects) {
|
||||
return
|
||||
}
|
||||
|
||||
const ids = this._getSelectValue()
|
||||
const objectsById = this._getObjectsById()
|
||||
|
||||
if (!isArray(ids)) {
|
||||
ids && !objectsById[ids] && onChange(undefined)
|
||||
} else {
|
||||
let shouldTriggerOnChange
|
||||
|
||||
const newValue = isArray(ids) && mapPlus(ids, (id, push) => {
|
||||
const object = objectsById[id]
|
||||
|
||||
if (object) {
|
||||
push(object)
|
||||
} else {
|
||||
shouldTriggerOnChange = true
|
||||
}
|
||||
})
|
||||
|
||||
if (shouldTriggerOnChange) {
|
||||
this.props.onChange(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_getObjectsById = createSelector(
|
||||
() => this.props.xoObjects,
|
||||
objects => keyBy(
|
||||
@@ -182,19 +150,17 @@ export class GenericSelect extends Component {
|
||||
(containers, objects) => { // createCollectionWrapper with a depth?
|
||||
const { name } = this.constructor
|
||||
|
||||
let options = []
|
||||
if (!containers) {
|
||||
if (__DEV__ && !isArray(objects)) {
|
||||
throw new Error(`${name}: without xoContainers, xoObjects must be an array`)
|
||||
}
|
||||
|
||||
return map(objects, getOption)
|
||||
}
|
||||
|
||||
if (__DEV__ && isArray(objects)) {
|
||||
options = map(objects, getOption)
|
||||
} else if (__DEV__ && isArray(objects)) {
|
||||
throw new Error(`${name}: with xoContainers, xoObjects must be an object`)
|
||||
}
|
||||
|
||||
const options = []
|
||||
forEach(containers, container => {
|
||||
options.push({
|
||||
disabled: true,
|
||||
@@ -205,6 +171,30 @@ export class GenericSelect extends Component {
|
||||
options.push(getOption(object, container))
|
||||
})
|
||||
})
|
||||
|
||||
const values = this._getSelectValue()
|
||||
const objectsById = this._getObjectsById()
|
||||
const addIfMissing = val => {
|
||||
if (val && !objectsById[val]) {
|
||||
options.push({
|
||||
disabled: true,
|
||||
id: val,
|
||||
label: val,
|
||||
value: val,
|
||||
xoItem: {
|
||||
id: val,
|
||||
removed: true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (isArray(values)) {
|
||||
forEach(values, addIfMissing)
|
||||
} else {
|
||||
addIfMissing(values)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
)
|
||||
|
||||
@@ -15,6 +15,7 @@ import mapValues from 'lodash/mapValues'
|
||||
import React from 'react'
|
||||
import ReadableStream from 'readable-stream'
|
||||
import replace from 'lodash/replace'
|
||||
import startsWith from 'lodash/startsWith'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import _ from './intl'
|
||||
@@ -63,13 +64,22 @@ export const addSubscriptions = subscriptions => Component => {
|
||||
|
||||
componentWillMount () {
|
||||
this._unsubscribes = map(isFunction(subscriptions) ? subscriptions() : subscriptions, (subscribe, prop) =>
|
||||
subscribe(value => this.setState({ [prop]: value }))
|
||||
subscribe(value => this._setState({ [prop]: value }))
|
||||
)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this._setState = this.setState
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
forEach(this._unsubscribes, unsubscribe => unsubscribe())
|
||||
this._unsubscribes = null
|
||||
delete this._setState
|
||||
}
|
||||
|
||||
_setState (nextState) {
|
||||
this.state = { ...this.state, nextState }
|
||||
}
|
||||
|
||||
render () {
|
||||
@@ -524,3 +534,6 @@ export const compareVersions = makeNiceCompare((v1, v2) => {
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
export const isXosanPack = ({ name }) =>
|
||||
startsWith(name, 'XOSAN')
|
||||
|
||||
@@ -16,10 +16,10 @@ export default class VmInput extends XoAbstractInput {
|
||||
disabled={props.disabled}
|
||||
hasSelectAll
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
value={props.value}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -1345,7 +1345,7 @@ export const disablePluginAutoload = id => (
|
||||
)
|
||||
)
|
||||
|
||||
export const configurePlugin = (id, configuration) => {
|
||||
export const configurePlugin = (id, configuration) =>
|
||||
_call('plugin.configure', { id, configuration })::tap(
|
||||
() => {
|
||||
info(_('pluginConfigurationSuccess'), _('pluginConfigurationChanges'))
|
||||
@@ -1354,7 +1354,6 @@ export const configurePlugin = (id, configuration) => {
|
||||
)::rethrow(
|
||||
err => error(_('pluginError'), JSON.stringify(err.data) || _('unknownPluginError'))
|
||||
)
|
||||
}
|
||||
|
||||
export const purgePluginConfiguration = async id => {
|
||||
await confirm({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import React from 'react'
|
||||
import { connectStore, compareVersions } from 'utils'
|
||||
import { connectStore, compareVersions, isXosanPack } from 'utils'
|
||||
import { subscribeResourceCatalog, subscribePlugins } from 'xo'
|
||||
import { createGetObjectsOfType, createSelector, createCollectionWrapper } from 'selectors'
|
||||
import { satisfies as versionSatisfies } from 'semver'
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
every,
|
||||
filter,
|
||||
forEach,
|
||||
map
|
||||
map,
|
||||
some
|
||||
} from 'lodash'
|
||||
|
||||
const findLatestPack = (packs, hostsVersions) => {
|
||||
@@ -37,11 +38,16 @@ const findLatestPack = (packs, hostsVersions) => {
|
||||
return latestPack
|
||||
}
|
||||
|
||||
@connectStore({
|
||||
@connectStore(() => ({
|
||||
hosts: createGetObjectsOfType('host').filter(
|
||||
(_, { pool }) => host => pool && host.$pool === pool.id && !host.supplementalPacks['vates:XOSAN']
|
||||
createSelector(
|
||||
(_, { pool }) => pool != null && pool.id,
|
||||
poolId => poolId
|
||||
? host => host.$pool === poolId && !some(host.supplementalPacks, isXosanPack)
|
||||
: false
|
||||
)
|
||||
)
|
||||
}, { withRef: true })
|
||||
}), { withRef: true })
|
||||
export default class InstallXosanPackModal extends Component {
|
||||
componentDidMount () {
|
||||
this._unsubscribePlugins = subscribePlugins(plugins => this.setState({ plugins }))
|
||||
|
||||
@@ -1,32 +1,38 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Component from 'base-component'
|
||||
import delay from 'lodash/delay'
|
||||
import forEach from 'lodash/forEach'
|
||||
import GenericInput from 'json-schema-input'
|
||||
import getEventValue from 'get-event-value'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import moment from 'moment-timezone'
|
||||
import React from 'react'
|
||||
import Scheduler, { SchedulePreview } from 'scheduling'
|
||||
import startsWith from 'lodash/startsWith'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import Wizard, { Section } from 'wizard'
|
||||
import { addSubscriptions } from 'utils'
|
||||
import { confirm } from 'modal'
|
||||
import { error } from 'notification'
|
||||
import { generateUiSchema } from 'xo-json-schema-input'
|
||||
import { SelectSubject } from 'select-objects'
|
||||
import { connectStore, EMPTY_OBJECT } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createSelector } from 'reselect'
|
||||
import { generateUiSchema } from 'xo-json-schema-input'
|
||||
import { getUser } from 'selectors'
|
||||
import { SelectSubject } from 'select-objects'
|
||||
import {
|
||||
forEach,
|
||||
identity,
|
||||
isArray,
|
||||
map,
|
||||
mapValues,
|
||||
noop,
|
||||
startsWith
|
||||
} from 'lodash'
|
||||
|
||||
import {
|
||||
createJob,
|
||||
createSchedule,
|
||||
getRemote,
|
||||
editJob,
|
||||
editSchedule,
|
||||
subscribeCurrentUser
|
||||
editSchedule
|
||||
} from 'xo'
|
||||
|
||||
// ===================================================================
|
||||
@@ -52,13 +58,13 @@ const NO_SMART_UI_SCHEMA = generateUiSchema(NO_SMART_SCHEMA)
|
||||
const SMART_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
status: {
|
||||
power_state: {
|
||||
default: 'All', // FIXME: can't translate
|
||||
enum: [ 'All', 'Running', 'Halted' ], // FIXME: can't translate
|
||||
title: _('editBackupSmartStatusTitle'),
|
||||
description: 'The statuses of VMs to backup.' // FIXME: can't translate
|
||||
},
|
||||
poolsOptions: {
|
||||
$pool: {
|
||||
type: 'object',
|
||||
title: _('editBackupSmartPools'),
|
||||
properties: {
|
||||
@@ -67,7 +73,7 @@ const SMART_SCHEMA = {
|
||||
title: _('editBackupNot'),
|
||||
description: 'Toggle on to backup VMs that are NOT resident on these pools'
|
||||
},
|
||||
pools: {
|
||||
values: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
@@ -78,7 +84,7 @@ const SMART_SCHEMA = {
|
||||
}
|
||||
}
|
||||
},
|
||||
tagsOptions: {
|
||||
tags: {
|
||||
type: 'object',
|
||||
title: _('editBackupSmartTags'),
|
||||
properties: {
|
||||
@@ -87,7 +93,7 @@ const SMART_SCHEMA = {
|
||||
title: _('editBackupNot'),
|
||||
description: 'Toggle on to backup VMs that do NOT contain these tags'
|
||||
},
|
||||
tags: {
|
||||
values: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
@@ -99,7 +105,7 @@ const SMART_SCHEMA = {
|
||||
}
|
||||
}
|
||||
},
|
||||
required: [ 'status', 'poolsOptions', 'tagsOptions' ]
|
||||
required: [ 'power_state', '$pool', 'tags' ]
|
||||
}
|
||||
const SMART_UI_SCHEMA = generateUiSchema(SMART_SCHEMA)
|
||||
|
||||
@@ -261,187 +267,178 @@ const BACKUP_METHOD_TO_INFO = {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const DEFAULT_CRON_PATTERN = '0 0 * * *'
|
||||
@uncontrollableInput()
|
||||
class TimeoutInput extends Component {
|
||||
_onChange = event => {
|
||||
const value = getEventValue(event).trim()
|
||||
this.props.onChange(value === '' ? null : +value * 1e3)
|
||||
}
|
||||
|
||||
function negatePattern (pattern, not = true) {
|
||||
render () {
|
||||
const { props } = this
|
||||
const { value } = props
|
||||
|
||||
return <input
|
||||
{...props}
|
||||
onChange={this._onChange}
|
||||
type='number'
|
||||
value={value == null ? '' : String(value / 1e3)}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const DEFAULT_CRON_PATTERN = '0 0 * * *'
|
||||
const DEFAULT_TIMEZONE = moment.tz.guess()
|
||||
|
||||
// xo-web v5.7.1 introduced a bug where an extra level
|
||||
// ({ id: { id: <id> } }) was introduced for the VM param.
|
||||
//
|
||||
// This code automatically unbox the ids.
|
||||
const extractId = value => {
|
||||
while (typeof value === 'object') {
|
||||
value = value.id
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
const destructPattern = (pattern, valueTransform = identity) => pattern && ({
|
||||
not: !!pattern.__not,
|
||||
values: valueTransform((pattern.__not || pattern).__or)
|
||||
})
|
||||
|
||||
const constructPattern = ({ not, values } = EMPTY_OBJECT, valueTransform = identity) => {
|
||||
if (values == null || !values.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const pattern = { __or: valueTransform(values) }
|
||||
return not
|
||||
? { __not: pattern }
|
||||
: pattern
|
||||
}
|
||||
|
||||
@addSubscriptions({
|
||||
currentUser: subscribeCurrentUser
|
||||
@connectStore({
|
||||
currentUser: getUser
|
||||
})
|
||||
export default class New extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state.cronPattern = DEFAULT_CRON_PATTERN
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
const { currentUser } = props
|
||||
const { owner } = this.state
|
||||
|
||||
if (currentUser && !owner) {
|
||||
this.setState({ owner: currentUser.id })
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
const { job, schedule } = this.props
|
||||
if (!job || !schedule) {
|
||||
if (job || schedule) { // Having only one of them is unexpected incomplete information
|
||||
error(_('backupEditNotFoundTitle'), _('backupEditNotFoundMessage'))
|
||||
_getParams = createSelector(
|
||||
() => this.props.job,
|
||||
job => {
|
||||
if (!job) {
|
||||
return { main: {}, vms: { vms: [] } }
|
||||
}
|
||||
this.setState({
|
||||
timezone: moment.tz.guess()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
backupInfo: BACKUP_METHOD_TO_INFO[job.method],
|
||||
cronPattern: schedule.cron,
|
||||
owner: job.userId,
|
||||
timeout: job.timeout && job.timeout / 1e3,
|
||||
timezone: schedule.timezone || null
|
||||
}, () => delay(this._populateForm, 250, job)) // Work around.
|
||||
// Without the delay, some selects are not always ready to load a value
|
||||
// Values are displayed, but html5 compliant browsers say the value is required and empty on submit
|
||||
}
|
||||
const { items } = job.paramsVector
|
||||
|
||||
_populateForm = job => {
|
||||
let values = job.paramsVector.items
|
||||
const {
|
||||
backupInput,
|
||||
vmsInput
|
||||
} = this.refs
|
||||
// legacy backup jobs
|
||||
if (items.length === 1) {
|
||||
const { ...main } = items[0].values[0]
|
||||
|
||||
if (values.length === 1) {
|
||||
// Older versions of XenOrchestra uses only values[0].
|
||||
const array = values[0].values
|
||||
const config = array[0]
|
||||
const reportWhen = config._reportWhen
|
||||
|
||||
backupInput.value = {
|
||||
...config,
|
||||
_reportWhen:
|
||||
// Fix old reportWhen values...
|
||||
(reportWhen === 'fail' && 'failure') ||
|
||||
(reportWhen === 'alway' && 'always') ||
|
||||
reportWhen
|
||||
return {
|
||||
main,
|
||||
vms: { vms: map(items[0].values.slice(1), extractId) }
|
||||
}
|
||||
}
|
||||
vmsInput.value = { vms: map(array, ({ id, vm }) => id || vm) }
|
||||
} else {
|
||||
if (values[1].type === 'map') {
|
||||
// Smart backup.
|
||||
const {
|
||||
$pool: poolsOptions = {},
|
||||
tags: tagsOptions = {},
|
||||
power_state: status = 'All'
|
||||
} = values[1].collection.pattern
|
||||
|
||||
backupInput.value = values[0].values[0]
|
||||
// smart backup
|
||||
if (items[1].type === 'map') {
|
||||
const { pattern } = items[1].collection
|
||||
const { $pool, tags } = pattern
|
||||
|
||||
this.setState({
|
||||
smartBackupMode: true
|
||||
}, () => {
|
||||
vmsInput.value = {
|
||||
poolsOptions: {
|
||||
pools: poolsOptions.__not ? poolsOptions.__not.__or : poolsOptions.__or,
|
||||
not: !!poolsOptions.__not
|
||||
},
|
||||
status,
|
||||
tagsOptions: {
|
||||
tags: map(tagsOptions.__not ? tagsOptions.__not.__or : tagsOptions.__or, tag => tag[0]),
|
||||
not: !!tagsOptions.__not
|
||||
}
|
||||
return {
|
||||
main: items[0].values[0],
|
||||
vms: {
|
||||
$pool: destructPattern($pool),
|
||||
power_state: pattern.power_state,
|
||||
tags: destructPattern(tags, tags => map(tags, tag => isArray(tag) ? tag[0] : tag))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Normal backup.
|
||||
backupInput.value = values[1].values[0]
|
||||
vmsInput.value = { vms: values[0].values }
|
||||
}
|
||||
}
|
||||
|
||||
// normal backup
|
||||
return {
|
||||
main: items[1].values[0],
|
||||
vms: { vms: map(items[0].values, extractId) }
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
_getMainParams = () => this.state.mainParams || this._getParams().main
|
||||
_getVmsParam = () => this.state.vmsParam || this._getParams().vms
|
||||
|
||||
_getScheduling = createSelector(
|
||||
() => this.props.schedule,
|
||||
() => this.state.scheduling,
|
||||
(schedule, scheduling) => {
|
||||
if (scheduling !== undefined) {
|
||||
return scheduling
|
||||
}
|
||||
|
||||
const {
|
||||
cron = DEFAULT_CRON_PATTERN,
|
||||
timezone = DEFAULT_TIMEZONE
|
||||
} = schedule || EMPTY_OBJECT
|
||||
|
||||
return {
|
||||
cronPattern: cron,
|
||||
timezone
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
_handleSubmit = async () => {
|
||||
const { props, state } = this
|
||||
|
||||
const method = this._getValue('job', 'method')
|
||||
const backupInfo = BACKUP_METHOD_TO_INFO[method]
|
||||
|
||||
const {
|
||||
enabled,
|
||||
...callArgs
|
||||
} = this.refs.backupInput.value
|
||||
const vmsInputValue = this.refs.vmsInput.value
|
||||
|
||||
const {
|
||||
backupInfo,
|
||||
smartBackupMode,
|
||||
timeout,
|
||||
timezone,
|
||||
owner
|
||||
} = this.state
|
||||
|
||||
const { pools, not: notPools } = vmsInputValue.poolsOptions || {}
|
||||
const { tags, not: notTags } = vmsInputValue.tagsOptions || {}
|
||||
const formattedTags = map(tags, tag => [ tag ])
|
||||
|
||||
const paramsVector = !smartBackupMode
|
||||
? {
|
||||
type: 'crossProduct',
|
||||
items: [{
|
||||
type: 'set',
|
||||
values: map(vmsInputValue.vms, vm => ({ id: vm }))
|
||||
}, {
|
||||
type: 'set',
|
||||
values: [ callArgs ]
|
||||
}]
|
||||
} : {
|
||||
type: 'crossProduct',
|
||||
items: [{
|
||||
type: 'set',
|
||||
values: [ callArgs ]
|
||||
}, {
|
||||
type: 'map',
|
||||
collection: {
|
||||
type: 'fetchObjects',
|
||||
pattern: {
|
||||
$pool: isEmpty(pools)
|
||||
? undefined
|
||||
: negatePattern({ __or: pools }, notPools),
|
||||
power_state: vmsInputValue.status === 'All' ? undefined : vmsInputValue.status,
|
||||
tags: isEmpty(tags)
|
||||
? undefined
|
||||
: negatePattern({ __or: formattedTags }, notTags),
|
||||
type: 'VM'
|
||||
}
|
||||
},
|
||||
iteratee: {
|
||||
type: 'extractProperties',
|
||||
mapping: { id: 'id' }
|
||||
}
|
||||
}]
|
||||
}
|
||||
...mainParams
|
||||
} = this._getMainParams()
|
||||
const vms = this._getVmsParam()
|
||||
|
||||
const job = {
|
||||
...props.job,
|
||||
...state.job,
|
||||
|
||||
type: 'call',
|
||||
key: backupInfo.jobKey,
|
||||
method: backupInfo.method,
|
||||
paramsVector,
|
||||
userId: owner,
|
||||
timeout: timeout ? timeout * 1e3 : undefined
|
||||
paramsVector: {
|
||||
type: 'crossProduct',
|
||||
items: isArray(vms.vms)
|
||||
? [{
|
||||
type: 'set',
|
||||
values: map(vms.vms, vm => ({ id: extractId(vm) }))
|
||||
}, {
|
||||
type: 'set',
|
||||
values: [ mainParams ]
|
||||
}]
|
||||
: [{
|
||||
type: 'set',
|
||||
values: [ mainParams ]
|
||||
}, {
|
||||
type: 'map',
|
||||
collection: {
|
||||
type: 'fetchObjects',
|
||||
pattern: {
|
||||
$pool: constructPattern(vms.$pool),
|
||||
power_state: vms.power_state === 'All' ? undefined : vms.power_state,
|
||||
tags: constructPattern(vms.tags, tags => map(tags, tag => [ tag ])),
|
||||
type: 'VM'
|
||||
}
|
||||
},
|
||||
iteratee: {
|
||||
type: 'extractProperties',
|
||||
mapping: { id: 'id' }
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
// Update backup schedule.
|
||||
const { job: oldJob, schedule: oldSchedule } = this.props
|
||||
|
||||
if (oldJob && oldSchedule) {
|
||||
job.id = oldJob.id
|
||||
return editJob(job).then(() => editSchedule({
|
||||
...oldSchedule,
|
||||
cron: this.state.cronPattern,
|
||||
timezone
|
||||
}))
|
||||
}
|
||||
const scheduling = this._getScheduling()
|
||||
|
||||
let remoteId
|
||||
if (job.type === 'call') {
|
||||
@@ -474,58 +471,79 @@ export default class New extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
// Update backup schedule.
|
||||
const oldJob = props.job
|
||||
if (oldJob) {
|
||||
job.id = oldJob.id
|
||||
await editJob(job)
|
||||
|
||||
return editSchedule({
|
||||
id: props.schedule.id,
|
||||
cron: scheduling.cronPattern,
|
||||
timezone: scheduling.timezone
|
||||
})
|
||||
}
|
||||
|
||||
if (job.timeout === null) {
|
||||
delete job.timeout // only needed for job edition
|
||||
}
|
||||
|
||||
// Create backup schedule.
|
||||
return createSchedule(await createJob(job), { cron: this.state.cronPattern, enabled, timezone })
|
||||
return createSchedule(await createJob(job), {
|
||||
cron: scheduling.cronPattern,
|
||||
enabled,
|
||||
timezone: scheduling.timezone
|
||||
})
|
||||
}
|
||||
|
||||
_handleReset = () => {
|
||||
const { backupInput } = this.refs
|
||||
|
||||
if (backupInput) {
|
||||
backupInput.value = undefined
|
||||
}
|
||||
|
||||
this.setState({
|
||||
cronPattern: DEFAULT_CRON_PATTERN
|
||||
})
|
||||
}
|
||||
|
||||
_updateCronPattern = value => {
|
||||
this.setState(value)
|
||||
}
|
||||
|
||||
_handleBackupSelection = event => {
|
||||
const method = event.target.value
|
||||
|
||||
this.setState({
|
||||
showVersionWarning: method === 'vm.rollingDeltaBackup' || method === 'vm.deltaCopy',
|
||||
backupInfo: BACKUP_METHOD_TO_INFO[method]
|
||||
})
|
||||
this.setState(mapValues(this.state, noop))
|
||||
}
|
||||
|
||||
_handleSmartBackupMode = event => {
|
||||
this.setState({
|
||||
smartBackupMode: event.target.value === 'smart'
|
||||
})
|
||||
this.setState(
|
||||
event.target.value === 'smart'
|
||||
? { vmsParam: {} }
|
||||
: { vmsParam: { vms: [] } }
|
||||
)
|
||||
}
|
||||
|
||||
_subjectPredicate = ({ type, permission }) =>
|
||||
type === 'user' && permission === 'admin'
|
||||
|
||||
render () {
|
||||
const { state } = this
|
||||
const {
|
||||
backupInfo,
|
||||
cronPattern,
|
||||
smartBackupMode,
|
||||
timezone,
|
||||
owner,
|
||||
showVersionWarning
|
||||
} = state
|
||||
_getValue = (ns, key, defaultValue) => {
|
||||
let tmp
|
||||
|
||||
return process.env.XOA_PLAN > 1
|
||||
? (
|
||||
<Wizard>
|
||||
// look in the state
|
||||
if (
|
||||
(tmp = this.state[ns]) != null &&
|
||||
(tmp = tmp[key]) !== undefined
|
||||
) {
|
||||
return tmp
|
||||
}
|
||||
|
||||
// look in the props
|
||||
if (
|
||||
(tmp = this.props[ns]) != null &&
|
||||
(tmp = tmp[key]) !== undefined
|
||||
) {
|
||||
return tmp
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
render () {
|
||||
const method = this._getValue('job', 'method', '')
|
||||
const scheduling = this._getScheduling()
|
||||
const vms = this._getVmsParam()
|
||||
|
||||
const backupInfo = BACKUP_METHOD_TO_INFO[method]
|
||||
const smartBackupMode = !isArray(vms.vms)
|
||||
|
||||
return (
|
||||
<Upgrade place='newBackup' required={2}>
|
||||
<Wizard><form id='form-new-vm-backup'>
|
||||
<Section icon='backup' title={this.props.job ? 'editVmBackup' : 'newVmBackup'}>
|
||||
<Container>
|
||||
<Row>
|
||||
@@ -533,92 +551,96 @@ export default class New extends Component {
|
||||
<fieldset className='form-group'>
|
||||
<label>{_('backupOwner')}</label>
|
||||
<SelectSubject
|
||||
onChange={this.linkState('owner', 'id')}
|
||||
onChange={this.linkState('job.userId', 'id')}
|
||||
predicate={this._subjectPredicate}
|
||||
required
|
||||
value={owner || null}
|
||||
value={this._getValue('job', 'userId', this.props.currentUser.id)}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset className='form-group'>
|
||||
<label>{_('jobTimeoutPlaceHolder')}</label>
|
||||
<input type='number' onChange={this.linkState('timeout')} value={state.timeout} className='form-control' />
|
||||
<TimeoutInput
|
||||
className='form-control'
|
||||
onChange={this.linkState('job.timeout')}
|
||||
value={this._getValue('job', 'timeout')}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset className='form-group'>
|
||||
<label htmlFor='selectBackup'>{_('newBackupSelection')}</label>
|
||||
<select
|
||||
className='form-control'
|
||||
value={(backupInfo && backupInfo.method) || ''}
|
||||
id='selectBackup'
|
||||
onChange={this._handleBackupSelection}
|
||||
onChange={this.linkState('job.method')}
|
||||
required
|
||||
value={method}
|
||||
>
|
||||
{_('noSelectedValue', message => <option value=''>{message}</option>)}
|
||||
{map(BACKUP_METHOD_TO_INFO, (info, key) =>
|
||||
_(info.label, message => <option key={key} value={key}>{message}</option>)
|
||||
_(info.label, message => <option key={key} value={key}>{message}</option>)
|
||||
)}
|
||||
</select>
|
||||
</fieldset>
|
||||
{showVersionWarning && <div className='alert alert-warning' role='alert'>
|
||||
{(method === 'vm.rollingDeltaBackup' || method === 'vm.deltaCopy') && <div className='alert alert-warning' role='alert'>
|
||||
<Icon icon='error' /> {_('backupVersionWarning')}
|
||||
</div>}
|
||||
<form id='form-new-vm-backup'>
|
||||
{backupInfo && <div>
|
||||
<GenericInput
|
||||
label={<span><Icon icon={backupInfo.icon} /> {_(backupInfo.label)}</span>}
|
||||
ref='backupInput'
|
||||
{backupInfo && <div>
|
||||
<GenericInput
|
||||
label={<span><Icon icon={backupInfo.icon} /> {_(backupInfo.label)}</span>}
|
||||
required
|
||||
schema={backupInfo.schema}
|
||||
uiSchema={backupInfo.uiSchema}
|
||||
onChange={this.linkState('mainParams')}
|
||||
value={this._getMainParams()}
|
||||
/>
|
||||
<fieldset className='form-group'>
|
||||
<label htmlFor='smartMode'>{_('smartBackupModeSelection')}</label>
|
||||
<select
|
||||
className='form-control'
|
||||
id='smartMode'
|
||||
onChange={this._handleSmartBackupMode}
|
||||
required
|
||||
schema={backupInfo.schema}
|
||||
uiSchema={backupInfo.uiSchema}
|
||||
/>
|
||||
<fieldset className='form-group'>
|
||||
<label htmlFor='smartMode'>{_('smartBackupModeSelection')}</label>
|
||||
<select
|
||||
className='form-control'
|
||||
id='smartMode'
|
||||
onChange={this._handleSmartBackupMode}
|
||||
required
|
||||
value={smartBackupMode ? 'smart' : 'normal'}
|
||||
>
|
||||
{_('normalBackup', message => <option value='normal'>{message}</option>)}
|
||||
{_('smartBackup', message => <option value='smart'>{message}</option>)}
|
||||
</select>
|
||||
</fieldset>
|
||||
{smartBackupMode
|
||||
? (process.env.XOA_PLAN > 2
|
||||
? <GenericInput
|
||||
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
|
||||
ref='vmsInput'
|
||||
required
|
||||
schema={SMART_SCHEMA}
|
||||
uiSchema={SMART_UI_SCHEMA}
|
||||
/>
|
||||
: <Container><Upgrade place='newBackup' available={3} /></Container>
|
||||
) : <GenericInput
|
||||
value={smartBackupMode ? 'smart' : 'normal'}
|
||||
>
|
||||
{_('normalBackup', message => <option value='normal'>{message}</option>)}
|
||||
{_('smartBackup', message => <option value='smart'>{message}</option>)}
|
||||
</select>
|
||||
</fieldset>
|
||||
{smartBackupMode
|
||||
? <Upgrade place='newBackup' required={3}>
|
||||
<GenericInput
|
||||
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
|
||||
ref='vmsInput'
|
||||
onChange={this.linkState('vmsParam')}
|
||||
required
|
||||
schema={NO_SMART_SCHEMA}
|
||||
uiSchema={NO_SMART_UI_SCHEMA}
|
||||
/>
|
||||
}
|
||||
</div>}
|
||||
</form>
|
||||
schema={SMART_SCHEMA}
|
||||
uiSchema={SMART_UI_SCHEMA}
|
||||
value={vms}
|
||||
/>
|
||||
</Upgrade>
|
||||
: <GenericInput
|
||||
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
|
||||
onChange={this.linkState('vmsParam')}
|
||||
required
|
||||
schema={NO_SMART_SCHEMA}
|
||||
uiSchema={NO_SMART_UI_SCHEMA}
|
||||
value={vms}
|
||||
/>
|
||||
}
|
||||
</div>}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Section>
|
||||
<Section icon='schedule' title='schedule'>
|
||||
<Scheduler
|
||||
cronPattern={cronPattern}
|
||||
onChange={this._updateCronPattern}
|
||||
timezone={timezone}
|
||||
onChange={this.linkState('scheduling')}
|
||||
value={scheduling}
|
||||
/>
|
||||
</Section>
|
||||
<Section icon='preview' title='preview' summary>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
<SchedulePreview cronPattern={cronPattern} />
|
||||
<SchedulePreview cronPattern={scheduling.cronPattern} />
|
||||
{process.env.XOA_PLAN < 4 && backupInfo && process.env.XOA_PLAN < REQUIRED_XOA_PLAN[backupInfo.jobKey]
|
||||
? <Upgrade place='newBackup' available={REQUIRED_XOA_PLAN[backupInfo.jobKey]} />
|
||||
: (smartBackupMode && process.env.XOA_PLAN < 3
|
||||
@@ -644,8 +666,8 @@ export default class New extends Component {
|
||||
</Row>
|
||||
</Container>
|
||||
</Section>
|
||||
</Wizard>
|
||||
)
|
||||
: <Container><Upgrade place='newBackup' available={2} /></Container>
|
||||
</form></Wizard>
|
||||
</Upgrade>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import _ from 'intl'
|
||||
import assign from 'lodash/assign'
|
||||
import HostActionBar from './action-bar'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import Link from 'link'
|
||||
import { NavLink, NavTabs } from 'nav'
|
||||
import Page from '../page'
|
||||
import pick from 'lodash/pick'
|
||||
import React, { cloneElement, Component } from 'react'
|
||||
import sortBy from 'lodash/sortBy'
|
||||
import sum from 'lodash/sum'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Text } from 'editable'
|
||||
import { editHost, fetchHostStats, getHostMissingPatches, installAllHostPatches, installHostPatch } from 'xo'
|
||||
@@ -25,6 +19,15 @@ import {
|
||||
createGetObjectsOfType,
|
||||
createSelector
|
||||
} from 'selectors'
|
||||
import {
|
||||
assign,
|
||||
isEmpty,
|
||||
isString,
|
||||
map,
|
||||
pick,
|
||||
sortBy,
|
||||
sum
|
||||
} from 'lodash'
|
||||
|
||||
import TabAdvanced from './tab-advanced'
|
||||
import TabConsole from './tab-console'
|
||||
@@ -94,7 +97,7 @@ const isRunning = host => host && host.power_state === 'Running'
|
||||
const getHostPatches = createSelector(
|
||||
createGetObjectsOfType('pool_patch'),
|
||||
createGetObjectsOfType('host_patch').pick(
|
||||
createSelector(getHost, host => host.patches)
|
||||
createSelector(getHost, host => isString(host.patches[0]) ? host.patches : [])
|
||||
),
|
||||
(poolsPatches, hostsPatches) => map(hostsPatches, hostPatch => ({
|
||||
...hostPatch,
|
||||
|
||||
@@ -16,14 +16,11 @@ const ALLOW_INSTALL_SUPP_PACK = process.env.XOA_PLAN > 1
|
||||
|
||||
const forceReboot = host => restartHost(host, true)
|
||||
|
||||
const formatPack = (version, pack) => {
|
||||
const [ author, name ] = pack.split(':')
|
||||
|
||||
return <tr>
|
||||
<th>{_('supplementalPackTitle', { author, name })}</th>
|
||||
<td>{version}</td>
|
||||
</tr>
|
||||
}
|
||||
const formatPack = ({ name, author, description, version }) => <tr>
|
||||
<th>{_('supplementalPackTitle', { author, name })}</th>
|
||||
<td>{description}</td>
|
||||
<td>{version}</td>
|
||||
</tr>
|
||||
|
||||
export default ({
|
||||
host
|
||||
|
||||
@@ -8,9 +8,9 @@ import map from 'lodash/map'
|
||||
import pick from 'lodash/pick'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import some from 'lodash/some'
|
||||
import StateButton from 'state-button'
|
||||
import TabButton from 'tab-button'
|
||||
import Tooltip from 'tooltip'
|
||||
import { ButtonGroup } from 'react-bootstrap-4/lib'
|
||||
import { confirm } from 'modal'
|
||||
import { connectStore, noop } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
@@ -166,34 +166,37 @@ class PifItem extends Component {
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{pif.carrier
|
||||
? <span className='tag tag-success'>
|
||||
{_('pifStatusConnected')}
|
||||
</span>
|
||||
: <span className='tag tag-default'>
|
||||
{_('pifStatusDisconnected')}
|
||||
</span>
|
||||
}
|
||||
<StateButton
|
||||
disabledLabel={_('pifDisconnected')}
|
||||
disabledHandler={connectPif}
|
||||
disabledTooltip={_('connectPif')}
|
||||
|
||||
enabledLabel={_('pifConnected')}
|
||||
enabledHandler={disconnectPif}
|
||||
enabledTooltip={_('disconnectPif')}
|
||||
|
||||
disabled={pif.attached && (pif.management || pif.disallowUnplug)}
|
||||
handlerParam={pif}
|
||||
state={pif.attached}
|
||||
/>
|
||||
{' '}
|
||||
<Tooltip content={pif.carrier ? _('pifPhysicallyConnected') : _('pifPhysicallyDisconnected')}>
|
||||
<Icon
|
||||
icon='network'
|
||||
size='lg'
|
||||
className={pif.carrier ? 'text-success' : 'text-muted'}
|
||||
/>
|
||||
</Tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<ButtonGroup className='pull-right'>
|
||||
<ActionRowButton
|
||||
btnStyle='default'
|
||||
disabled={pif.attached && (pif.management || pif.disallowUnplug)}
|
||||
handler={pif.attached ? disconnectPif : connectPif}
|
||||
handlerParam={pif}
|
||||
icon={pif.attached ? 'disconnect' : 'connect'}
|
||||
tooltip={pif.attached ? _('disconnectPif') : _('connectPif')}
|
||||
/>
|
||||
<ActionRowButton
|
||||
btnStyle='default'
|
||||
disabled={pif.physical || pif.disallowUnplug || pif.management}
|
||||
handler={deletePif}
|
||||
handlerParam={pif}
|
||||
icon='delete'
|
||||
tooltip={_('deletePif')}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
<td className='text-xs-right'>
|
||||
<ActionRowButton
|
||||
btnStyle='default'
|
||||
disabled={pif.physical || pif.disallowUnplug || pif.management}
|
||||
handler={deletePif}
|
||||
handlerParam={pif}
|
||||
icon='delete'
|
||||
tooltip={_('deletePif')}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@@ -232,7 +235,7 @@ export default (({
|
||||
<th>{_('pifMtuLabel')}</th>
|
||||
<th>{_('defaultLockingMode')}</th>
|
||||
<th>{_('pifStatusLabel')}</th>
|
||||
<th />
|
||||
<th className='text-xs-right'>{_('pifAction')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import _ from 'intl'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import React, { Component } from 'react'
|
||||
import SortedTable from 'sorted-table'
|
||||
import TabButton from 'tab-button'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { connectStore, formatSize } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createDoesHostNeedRestart } from 'selectors'
|
||||
import { createDoesHostNeedRestart, createSelector } from 'selectors'
|
||||
import { FormattedRelative, FormattedTime } from 'react-intl'
|
||||
import { restartHost } from 'xo'
|
||||
import {
|
||||
isEmpty,
|
||||
isString
|
||||
} from 'lodash'
|
||||
|
||||
const MISSING_PATCH_COLUMNS = [
|
||||
{
|
||||
@@ -84,12 +87,56 @@ const INSTALLED_PATCH_COLUMNS = [
|
||||
}
|
||||
]
|
||||
|
||||
// support for software_version.platform_version ^2.1.1
|
||||
const INSTALLED_PATCH_COLUMNS_2 = [
|
||||
{
|
||||
default: true,
|
||||
name: _('patchNameLabel'),
|
||||
itemRenderer: patch => patch.name,
|
||||
sortCriteria: patch => patch.name
|
||||
},
|
||||
{
|
||||
name: _('patchDescription'),
|
||||
itemRenderer: patch => patch.description,
|
||||
sortCriteria: patch => patch.description
|
||||
},
|
||||
{
|
||||
name: _('patchSize'),
|
||||
itemRenderer: patch => formatSize(patch.size),
|
||||
sortCriteria: patch => patch.size
|
||||
}
|
||||
]
|
||||
|
||||
@connectStore(() => ({
|
||||
needsRestart: createDoesHostNeedRestart((_, props) => props.host)
|
||||
}))
|
||||
export default class HostPatches extends Component {
|
||||
_getPatches = createSelector(
|
||||
() => this.props.host,
|
||||
() => this.props.hostPatches,
|
||||
(host, hostPatches) => {
|
||||
if (isEmpty(host.patches) && isEmpty(hostPatches)) {
|
||||
return { patches: null }
|
||||
}
|
||||
|
||||
if (isString(host.patches[0])) {
|
||||
return {
|
||||
patches: hostPatches,
|
||||
columns: INSTALLED_PATCH_COLUMNS
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
patches: host.patches,
|
||||
columns: INSTALLED_PATCH_COLUMNS_2
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
render () {
|
||||
const { host, hostPatches, missingPatches, installAllPatches, installPatch } = this.props
|
||||
const { host, missingPatches, installAllPatches, installPatch } = this.props
|
||||
const { patches, columns } = this._getPatches()
|
||||
|
||||
return process.env.XOA_PLAN > 1
|
||||
? <Container>
|
||||
<Row>
|
||||
@@ -125,13 +172,12 @@ export default class HostPatches extends Component {
|
||||
</Row>}
|
||||
<Row>
|
||||
<Col>
|
||||
{!isEmpty(hostPatches)
|
||||
? (
|
||||
<span>
|
||||
<h3>{_('hostAppliedPatches')}</h3>
|
||||
<SortedTable collection={hostPatches} columns={INSTALLED_PATCH_COLUMNS} />
|
||||
</span>
|
||||
) : <h4 className='text-xs-center'>{_('patchNothing')}</h4>
|
||||
{patches
|
||||
? <span>
|
||||
<h3>{_('hostAppliedPatches')}</h3>
|
||||
<SortedTable collection={patches} columns={columns} />
|
||||
</span>
|
||||
: <h4 className='text-xs-center'>{_('patchNothing')}</h4>
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -54,7 +54,7 @@ class JobParam extends Component {
|
||||
|
||||
return object
|
||||
? <span><strong>{object.type || paramKey}</strong>: {renderXoItem(object)} </span>
|
||||
: <span><strong>{paramKey}:</strong> {id} </span>
|
||||
: <span><strong>{paramKey}:</strong> {String(id)} </span>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1116,6 +1116,7 @@ export default class NewVm extends BaseComponent {
|
||||
<SectionContent column>
|
||||
{map(VIFs, (vif, index) => <div key={index}>
|
||||
<Vif
|
||||
networkPredicate={this._getNetworkPredicate()}
|
||||
onChangeAddresses={this._linkState(`VIFs.${index}.addresses`, '*.id')}
|
||||
onChangeMac={this._linkState(`VIFs.${index}.mac`)}
|
||||
onChangeNetwork={this._linkState(`VIFs.${index}.network`, 'id')}
|
||||
@@ -1323,7 +1324,7 @@ export default class NewVm extends BaseComponent {
|
||||
<Tags labels={tags} onChange={this._linkState('tags')} />
|
||||
</Item>
|
||||
</SectionContent>,
|
||||
<SectionContent>
|
||||
this._getResourceSet() !== undefined && <SectionContent>
|
||||
<Item>
|
||||
<input
|
||||
checked={share}
|
||||
|
||||
@@ -372,6 +372,7 @@ export default class User extends Component {
|
||||
<option value='en'>English</option>
|
||||
<option value='fr'>Français</option>
|
||||
<option value='he'>עברי</option>
|
||||
<option value='pl'>Polski</option>
|
||||
<option value='pt'>Português</option>
|
||||
<option value='es'>Español</option>
|
||||
<option value='zh'>简体中文</option>
|
||||
|
||||
@@ -82,7 +82,7 @@ class NewDisk extends Component {
|
||||
.then(diskId => {
|
||||
const mode = readOnly.value ? 'RO' : 'RW'
|
||||
return attachDiskToVm(diskId, vm, {
|
||||
bootable: bootable.value,
|
||||
bootable: bootable && bootable.value,
|
||||
mode
|
||||
})
|
||||
.then(onClose)
|
||||
@@ -164,7 +164,7 @@ class AttachDisk extends Component {
|
||||
})
|
||||
const mode = readOnly.value || !_isFreeForWriting(vdi) ? 'RO' : 'RW'
|
||||
return attachDiskToVm(vdi, vm, {
|
||||
bootable: bootable.value,
|
||||
bootable: bootable && bootable.value,
|
||||
mode
|
||||
})
|
||||
.then(onClose)
|
||||
|
||||
@@ -19,7 +19,8 @@ import {
|
||||
isEmpty,
|
||||
keys,
|
||||
map,
|
||||
pickBy
|
||||
pickBy,
|
||||
some
|
||||
} from 'lodash'
|
||||
import {
|
||||
createGetObjectsOfType,
|
||||
@@ -31,7 +32,9 @@ import {
|
||||
addSubscriptions,
|
||||
compareVersions,
|
||||
connectStore,
|
||||
formatSize
|
||||
formatSize,
|
||||
isXosanPack,
|
||||
mapPlus
|
||||
} from 'utils'
|
||||
import {
|
||||
computeXosanPossibleOptions,
|
||||
@@ -85,7 +88,7 @@ export class XosanVolumesTable extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { xosansrs } = this.props
|
||||
const { xosansrs, hosts } = this.props
|
||||
return <div>
|
||||
<h3>{_('xosanSrTitle')}</h3>
|
||||
<table className='table table-striped'>
|
||||
@@ -101,13 +104,14 @@ export class XosanVolumesTable extends Component {
|
||||
<tbody>
|
||||
{map(xosansrs, sr => {
|
||||
const configsMap = {}
|
||||
sr.PBDs.forEach(pbd => { configsMap[pbd.device_config['server']] = true })
|
||||
forEach(sr.pbds, pbd => { configsMap[pbd.device_config['server']] = true })
|
||||
|
||||
return <tr key={sr.id}>
|
||||
<td>
|
||||
<Link to={`/srs/${sr.id}/xosan`}>{sr.name_label}</Link>
|
||||
</td>
|
||||
<td>
|
||||
{ sr.PBDs.map(pbd => pbd.realHost.name_label).join(', ') }
|
||||
{ map(sr.pbds, ({ host }) => find(hosts, [ 'id', host ]).name_label).join(', ') }
|
||||
</td>
|
||||
<td>
|
||||
{ this.state.volumeConfig && this.state.volumeConfig[sr.id] && this.state.volumeConfig[sr.id]['Volume ID'] }
|
||||
@@ -231,6 +235,7 @@ class PoolAvailableSrs extends Component {
|
||||
|
||||
render () {
|
||||
const {
|
||||
hosts,
|
||||
lvmsrs,
|
||||
noPack,
|
||||
pool
|
||||
@@ -273,7 +278,8 @@ class PoolAvailableSrs extends Component {
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(lvmsrs, sr => {
|
||||
const host = sr.PBDs[0].realHost
|
||||
const host = find(hosts, [ 'id', sr.$container ])
|
||||
|
||||
return <tr key={sr.id}>
|
||||
<td>
|
||||
<input
|
||||
@@ -393,45 +399,31 @@ class PoolAvailableSrs extends Component {
|
||||
// ==================================================================
|
||||
|
||||
@connectStore(() => {
|
||||
const pools = createGetObjectsOfType('pool')
|
||||
const pbdsBySr = createGetObjectsOfType('PBD').groupBy('SR')
|
||||
|
||||
const hosts = createGetObjectsOfType('host').groupBy('$pool')
|
||||
|
||||
const lvmSrsByPool = createGroupBy(createSort(createSelector(
|
||||
createGetObjectsOfType('SR').filter([sr => !sr.shared && sr.SR_type === 'lvm']),
|
||||
createGetObjectsOfType('PBD').groupBy('SR'),
|
||||
createGetObjectsOfType('host'),
|
||||
(srs, pbds, hosts) => map(srs, sr => {
|
||||
const list = pbds[sr.id]
|
||||
sr.PBDs = list || []
|
||||
sr.PBDs.forEach(pbd => {
|
||||
pbd.realHost = hosts[pbd.host]
|
||||
})
|
||||
sr.PBDs.sort()
|
||||
return sr
|
||||
}).filter(sr => Boolean(sr.PBDs.length))
|
||||
), 'name_label'), '$pool')
|
||||
|
||||
const xosanSrsByPool = createGroupBy(createSort(createSelector(
|
||||
createGetObjectsOfType('SR').filter([sr => sr.shared && sr.SR_type === 'xosan']),
|
||||
createGetObjectsOfType('PBD').groupBy('SR'),
|
||||
createGetObjectsOfType('host'),
|
||||
(srs, pbds, hosts) => map(srs, sr => {
|
||||
const list = pbds[sr.id]
|
||||
sr.PBDs = list || []
|
||||
sr.PBDs.forEach(pbd => {
|
||||
pbd.realHost = hosts[pbd.host]
|
||||
})
|
||||
sr.PBDs.sort((pbd1, pbd2) => pbd1.realHost.name_label.localeCompare(pbd2.realHost.name_label))
|
||||
return sr
|
||||
const lvmSrs = createSort(createSelector(
|
||||
createGetObjectsOfType('SR').filter([ sr => !sr.shared && sr.SR_type === 'lvm' ]),
|
||||
pbdsBySr,
|
||||
(srs, pbdsBySr) => mapPlus(srs, (sr, push) => {
|
||||
let pbds
|
||||
if ((pbds = pbdsBySr[sr.id]).length) {
|
||||
push({ ...sr, pbds })
|
||||
}
|
||||
})
|
||||
), 'name_label'), '$pool')
|
||||
), 'name_label')
|
||||
|
||||
const xosanSrs = createSort(createSelector(
|
||||
createGetObjectsOfType('SR').filter([sr => sr.shared && sr.SR_type === 'xosan']),
|
||||
pbdsBySr,
|
||||
(srs, pbdsBySr) =>
|
||||
map(srs, sr => ({ ...sr, pbds: pbdsBySr[sr.id] }))
|
||||
), 'name_label')
|
||||
|
||||
return {
|
||||
hosts,
|
||||
pools,
|
||||
xosanSrsByPool,
|
||||
lvmSrsByPool,
|
||||
hostsByPool: createGetObjectsOfType('host').groupBy('$pool'),
|
||||
pools: createGetObjectsOfType('pool'),
|
||||
xosanSrsByPool: createGroupBy(xosanSrs, '$pool'),
|
||||
lvmSrsByPool: createGroupBy(lvmSrs, '$pool'),
|
||||
networks: createGetObjectsOfType('network').groupBy('$pool')
|
||||
}
|
||||
})
|
||||
@@ -473,7 +465,7 @@ export default class Xosan extends Component {
|
||||
)
|
||||
|
||||
render () {
|
||||
const { pools, xosanSrsByPool, lvmSrsByPool, catalog } = this.props
|
||||
const { pools, xosanSrsByPool, lvmSrsByPool, catalog, hostsByPool } = this.props
|
||||
const error = this._getError()
|
||||
|
||||
return <Page header={HEADER} title='xosan' formatTitle>
|
||||
@@ -484,14 +476,14 @@ export default class Xosan extends Component {
|
||||
: map(pools, pool => {
|
||||
const poolXosanSrs = xosanSrsByPool[pool.id]
|
||||
const poolLvmSrs = lvmSrsByPool[pool.id]
|
||||
// TODO: check hosts supplementalPacks directly instead of checking each SR
|
||||
const noPack = !every(poolLvmSrs, sr => sr.PBDs[0].realHost.supplementalPacks['vates:XOSAN'])
|
||||
const hosts = hostsByPool[pool.id]
|
||||
const noPack = !every(hosts, host => some(host.supplementalPacks, isXosanPack))
|
||||
|
||||
return <Collapse key={pool.id} className='mb-1' buttonText={<span>{noPack && <Icon icon='error' />} {pool.name_label}</span>}>
|
||||
<div className='m-1'>
|
||||
{poolXosanSrs && poolXosanSrs.length
|
||||
? <XosanVolumesTable xosansrs={poolXosanSrs} lvmsrs={poolLvmSrs} />
|
||||
: <PoolAvailableSrs pool={pool} lvmsrs={poolLvmSrs} noPack={noPack} templates={filter(catalog.xosan, { type: 'xva' })} />
|
||||
? <XosanVolumesTable hosts={hosts} xosansrs={poolXosanSrs} lvmsrs={poolLvmSrs} />
|
||||
: <PoolAvailableSrs hosts={hosts} pool={pool} lvmsrs={poolLvmSrs} noPack={noPack} templates={filter(catalog.xosan, { type: 'xva' })} />
|
||||
}
|
||||
</div>
|
||||
</Collapse>
|
||||
|
||||
Reference in New Issue
Block a user