Compare commits

...

16 Commits

Author SHA1 Message Date
Julien Fontanet
bb8f96c2e2 5.0.2 2016-06-28 14:38:13 +02:00
Julien Fontanet
95d4cc9055 chore(README): master → stable 2016-06-28 14:37:14 +02:00
Julien Fontanet
cb84a85f8b chore(package): make package publishable 2016-06-28 14:36:32 +02:00
Pierre Donias
0a8aa2ecf5 feat(user): better UI and password edition (#1165)
Fixes #1127
2016-06-28 14:32:02 +02:00
Julien Fontanet
5941321e84 fix(intl/locales): Spanish is es, not sp 2016-06-28 14:15:11 +02:00
Julien Fontanet
8cf62280f4 feat(intl/locales/sp): initial file 2016-06-28 14:13:21 +02:00
Olivier Lambert
4cea142b57 fix(tasks): improve the task view (#1166)
Fixes #1147
2016-06-28 12:31:25 +02:00
Fabrice Marsaud
64d9245bc4 fix(settings/users): correctly set default permission value (#1170)
Fixes #1159
2016-06-28 11:49:04 +02:00
Fabrice Marsaud
2d78c0c4c3 fix(backup/restore): ignore incorrectly formatted files (#1163)
Fixes #1164
2016-06-28 11:19:54 +02:00
Pierre Donias
aa585e2d25 fix(home): always use advanced migration modal (#1137) 2016-06-27 16:15:52 +02:00
Julien Fontanet
325ab17dcc chore(xo): prefix local function call with _ 2016-06-27 16:04:54 +02:00
Pierre Donias
443ea44bcd fix(new/vm): default custom cloud config (#1125) 2016-06-27 15:21:20 +02:00
Pierre Donias
07958d8efa fix(vms/new): gracefully handle missing objects (#1124) 2016-06-27 15:20:05 +02:00
Olivier Lambert
f19affe599 fix: better SR predicate (#1122) 2016-06-27 15:15:46 +02:00
Julien Fontanet
f7b7c27b6c fix(host/storage): use long clicks for SR name edition 2016-06-25 09:10:22 +02:00
Julien Fontanet
c7af5b384c fix(xo/editSr): use camel case param
Fixes #1116
2016-06-25 09:09:12 +02:00
25 changed files with 2469 additions and 238 deletions

View File

@@ -60,8 +60,8 @@ Otherwise, please consider using the [bugtracker of the general repository](http
## Process for new release
```bash
# Switch to the master branch.
git checkout master
# Switch to the stable branch.
git checkout stable
# Fetches latest changes.
git pull --ff-only
@@ -75,12 +75,12 @@ npm version minor
# Go back to the next-release branch.
git checkout next-release
# Fetches the last changes (the merge and version bump) from master to
# Fetches the last changes (the merge and version bump) from stable to
# next-release.
git merge --ff-only master
git merge --ff-only stable
# Push the changes on git.
git push --follow-tags origin master next-release
git push --follow-tags origin stable next-release
# Publish this release to npm.
npm publish

View File

@@ -1,7 +1,7 @@
{
"private": true,
"private": false,
"name": "xo-web",
"version": "5.0.1",
"version": "5.0.2",
"license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra",
"keywords": [
@@ -111,7 +111,7 @@
"superagent": "^2.0.0",
"vinyl": "^1.1.1",
"watchify": "^3.7.0",
"xo-acl-resolver": "^0.2.0",
"xo-acl-resolver": "^0.2.1",
"xo-lib": "^0.8.0-1",
"xo-remote-parser": "^0.3"
},

File diff suppressed because it is too large Load Diff

View File

@@ -367,7 +367,6 @@ export default {
restartVmsModalTitle: 'Redémarrer {vms, plural, one {la} other {les}} VM{vms, plural, one {} other {s}}',
restartVmsModalMessage: 'Voulez-vous vraiment redémarrer {vms} VM{vms, plural, one {} other {s}} ?',
migrateVmModalTitle: 'Migrer la VM',
migrateVmModalBody: 'Voulez-vous vraiment migrer cette VM sur {hostName} ?',
migrateVmAdvancedModalSelectHost: 'Sélectionnez un hôte de destination:',
migrateVmAdvancedModalSelectNetwork: 'Sélectionnez un réseau pour la migration:',
migrateVmAdvancedModalSelectSrs: 'Pour chaque VDI, sélectionnez un SR:',

View File

@@ -1715,9 +1715,6 @@ export default {
// Original text: "Migrate VM"
migrateVmModalTitle: undefined,
// Original text: "Are you sure you want to migrate this VM to {hostName}?"
migrateVmModalBody: undefined,
// Original text: "Select a destination host:"
migrateVmAdvancedModalSelectHost: undefined,

View File

@@ -1712,9 +1712,6 @@ export default {
// Original text: "Migrate VM"
migrateVmModalTitle: 'Migrar VM',
// Original text: "Are you sure you want to migrate this VM to {hostName}?"
migrateVmModalBody: 'Você tem certeza que deseja migrar esta VM para {hostName}?',
// Original text: "Select a destination host:"
migrateVmAdvancedModalSelectHost: 'Selecionar um host de destino:',

View File

@@ -55,6 +55,7 @@ var messages = {
jobsNewPage: 'New',
jobsSchedulingPage: 'Scheduling',
customJob: 'Custom Job',
userPage: 'User',
// ----- Sign in/out -----
usernameLabel: 'Username:',
@@ -677,7 +678,6 @@ var messages = {
deleteVmModalMessage: 'Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED',
deleteVmsModalMessage: 'Are you sure you want to delete {vms} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED',
migrateVmModalTitle: 'Migrate VM',
migrateVmModalBody: 'Are you sure you want to migrate this VM to {hostName}?',
migrateVmAdvancedModalSelectHost: 'Select a destination host:',
migrateVmAdvancedModalSelectNetwork: 'Select a migration network:',
migrateVmAdvancedModalSelectSrs: 'For each VDI, select an SR:',
@@ -793,7 +793,22 @@ var messages = {
disconnectPif: 'Disconnect PIF',
disconnectPifConfirm: 'Are you sure you want to disconnect this PIF?',
deletePif: 'Delete PIF',
deletePifConfirm: 'Are you sure you want to delete this PIF?'
deletePifConfirm: 'Are you sure you want to delete this PIF?',
// ----- User -----
username: 'Username',
password: 'Password',
language: 'Language',
oldPasswordPlaceholder: 'Old password',
newPasswordPlaceholder: 'New password',
confirmPasswordPlaceholder: 'Confirm new password',
confirmationPasswordError: 'Confirmation password incorrect',
confirmationPasswordErrorBody: 'Password does not match the confirm password.',
pwdChangeSuccess: 'Password changed',
pwdChangeSuccessBody: 'Your password has been successfully changed.',
pwdChangeError: 'Incorrect password',
pwdChangeErrorBody: 'The old password provided is incorrect. Your password has not been changed.',
changePasswordOk: 'OK'
}
forEach(messages, function (message, id) {
if (isString(message)) {

View File

@@ -1,7 +1,7 @@
import Icon from 'icon'
import React, { Component } from 'react'
import { createGetObject } from 'selectors'
import { isSrWritable } from 'xo'
import {
connectStore,
formatSize,
@@ -53,7 +53,7 @@ export const SrItem = propTypes({
})(({ sr, container }) => {
let label = `${sr.name_label || sr.id}`
if (sr.content_type === 'user') {
if (isSrWritable(sr)) {
label += ` (${formatSize(sr.size)})`
}

View File

@@ -28,6 +28,7 @@ import {
} from 'utils'
import {
isSrWritable,
subscribeGroups,
subscribeRemotes,
subscribeRoles,
@@ -325,11 +326,9 @@ export const SelectPool = makeStoreSelect(() => ({
// ===================================================================
const userSrPredicate = sr => sr.content_type === 'user'
export const SelectSr = makeStoreSelect(() => {
const getSrsByContainer = createGetObjectsOfType('SR').filter(
(_, { predicate }) => predicate || userSrPredicate
(_, { predicate }) => predicate || isSrWritable
).sort().groupBy('$container')
const getContainerIds = createSelector(

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,8 @@ import {
createSelector
} from '../../selectors'
import { isSrWritable } from '../'
import styles from './index.css'
@connectStore(() => {
@@ -76,7 +78,7 @@ export default class MigrateVmModalBody extends Component {
this._getSrPredicate = createSelector(
() => this.state.host,
host => (host
? sr => sr.content_type !== 'iso' && (sr.$container === host.id || sr.$container === host.$pool)
? sr => isSrWritable(sr) && (sr.$container === host.id || sr.$container === host.$pool)
: () => false
)
)
@@ -101,6 +103,10 @@ export default class MigrateVmModalBody extends Component {
)
}
componentDidMount () {
this._selectHost(this.props.host)
}
get value () {
return {
targetHost: this.state.host && this.state.host.id,
@@ -154,13 +160,14 @@ export default class MigrateVmModalBody extends Component {
_selectMigrationNetwork = network => this.setState({ network })
render () {
const { vdis, vifs, networks } = this.props
const { host, vdis, vifs, networks } = this.props
return <div>
<div className={styles.firstBlock}>
<SingleLineRow>
<Col size={6}>{_('migrateVmAdvancedModalSelectHost')}</Col>
<Col size={6}>
<SelectHost
defaultValue={host}
onChange={this._selectHost}
predicate={this._getHostPredicate()}
/>

View File

@@ -343,6 +343,18 @@
@extend .xo-status-busy;
}
// Task
&-task {
&-cancel {
@extend .fa;
@extend .fa-ban;
}
&-destroy {
@extend .fa;
@extend .fa-trash;
}
}
// SR
&-sr, &-vdi {
&-reconnect-all {

View File

@@ -25,6 +25,7 @@ import { SelectSr } from 'select-objects'
import {
importBackup,
importDeltaBackup,
isSrWritable,
listRemote,
startVm,
subscribeRemotes
@@ -43,7 +44,7 @@ const backupOptionRenderer = backup => <span>
@connectStore(() => ({
writableSrs: createGetObjectsOfType('SR').filter(
[ sr => sr.content_type !== 'iso' ]
[ isSrWritable ]
).sort()
}))
export default class Restore extends Component {
@@ -102,8 +103,10 @@ export default class Restore extends Component {
}
}
}
backupInfoByVm[backup.name] || (backupInfoByVm[backup.name] = [])
backupInfoByVm[backup.name].push(backup)
if (backup) {
backupInfoByVm[backup.name] || (backupInfoByVm[backup.name] = [])
backupInfoByVm[backup.name].push(backup)
}
})
for (let vm in backupInfoByVm) {
const bks = backupInfoByVm[vm]
@@ -208,12 +211,11 @@ const BK_COLUMNS = [
}
]
const srWritablePredicate = sr => sr.content_type !== 'iso'
const notifyImportStart = () => info(_('importBackupTitle'), _('importBackupMessage'))
@connectStore(() => ({
writableSrs: createGetObjectsOfType('SR').filter(
[ sr => sr.content_type !== 'iso' ]
[ isSrWritable ]
).sort()
}), { withRef: true })
class _ModalBody extends Component {
@@ -274,7 +276,7 @@ class _ModalBody extends Component {
render () {
return <div>
<SelectSr ref='sr' predicate={srWritablePredicate} />
<SelectSr ref='sr' predicate={isSrWritable} />
<br />
<SelectPlainObject ref='backup' options={this.state.options} optionKey='path' optionRenderer={backupOptionRenderer} placeholder={this.props.intl.formatMessage(messages.importBackupModalSelectBackup)} />
<br />

View File

@@ -8,7 +8,7 @@ import TabButton from 'tab-button'
import Upgrade from 'xoa-upgrade'
import React, { Component } from 'react'
import { confirm } from 'modal'
import { deleteMessage, deleteVdi, deleteVm } from 'xo'
import { deleteMessage, deleteVdi, deleteVm, isSrWritable } from 'xo'
import { FormattedRelative, FormattedTime } from 'react-intl'
import { Container, Row, Col } from 'grid'
import {
@@ -182,7 +182,7 @@ const ALARM_COLUMNS = [
.filter([ snapshot => !snapshot.$snapshot_of ])
.sort()
const getUserSrs = createGetObjectsOfType('SR')
.filter([ sr => sr.content_type === 'user' ])
.filter([ isSrWritable ])
const getVdiSrs = createGetObjectsOfType('SR')
.pick(createSelector(
getOrphanVdiSnapshots,

View File

@@ -18,6 +18,7 @@ import {
formatSize
} from 'utils'
import {
isSrWritable,
subscribeUsers
} from 'xo'
@@ -46,7 +47,7 @@ import {
const userSrs = createTop(
createGetObjectsOfType('SR').filter(
[ sr => sr.content_type === 'user' ]
[ isSrWritable ]
),
[ sr => sr.physical_usage / sr.size ],
5

View File

@@ -44,7 +44,7 @@ export default ({
const sr = srs[pbd.SR]
return <BlockLink key={pbd.id} to={`/srs/${sr.id}/general`} tagName='tr'>
<td>
<Text value={sr.name_label} onChange={value => editSr(sr, { name_label: value })} />
<Text value={sr.name_label} onChange={nameLabel => editSr(sr, { nameLabel })} useLongClick />
</td>
<td>{sr.SR_type}</td>
<td>{formatSize(sr.size)}</td>

View File

@@ -67,3 +67,7 @@
.refreshNames {
cursor: pointer;
}
.customConfig {
resize: both;
}

View File

@@ -237,7 +237,7 @@ export default class NewVm extends BaseComponent {
_getSrPredicate = createSelector(
() => this.state.state.pool.id,
poolId => disk => disk.$pool === poolId && disk.content_type === 'user'
poolId => disk => disk.$pool === poolId && disk.content_type !== 'iso' && disk.size > 0
)
_getDefaultNetworkId = () => {
@@ -260,15 +260,17 @@ export default class NewVm extends BaseComponent {
const existingDisks = {}
forEach(template.$VBDs, vbdId => {
const vbd = getObject(storeState, vbdId)
if (vbd.is_cd_drive) {
if (!vbd || vbd.is_cd_drive) {
return
}
const vdi = getObject(storeState, vbd.VDI)
existingDisks[this.getUniqueId()] = {
name_label: vdi.name_label,
name_description: vdi.name_description,
size: vdi.size,
$SR: vdi.$SR
if (vdi) {
existingDisks[this.getUniqueId()] = {
name_label: vdi.name_label,
name_description: vdi.name_description,
size: vdi.size,
$SR: vdi.$SR
}
}
})
@@ -301,6 +303,7 @@ export default class NewVm extends BaseComponent {
cpuWeight: 1,
// installation
installMethod: template.install_methods && template.install_methods[0] || state.installMethod,
customConfig: '#cloud-config\n#hostname: myhostname\n#ssh_authorized_keys:\n# - ssh-rsa <myKey>\n#packages:\n# - htop\n',
// interfaces
VIFs,
// disks
@@ -361,7 +364,11 @@ export default class NewVm extends BaseComponent {
value = isArray(value) ? [ ...value ] : { ...value }
let eventValue = getEventValue(event)
eventValue = targetObjectProp ? eventValue[targetObjectProp] : eventValue
stateObjectProp ? value[index][stateObjectProp] = eventValue : value[index] = eventValue
if (value[index] && stateObjectProp) {
value[index][stateObjectProp] = eventValue
} else {
value[index] = eventValue
}
} else {
value = getEventValue(event)
}
@@ -599,7 +606,7 @@ export default class NewVm extends BaseComponent {
<span>{_('newVmCustomConfig')}</span>
{' '}
<DebounceInput
className='form-control'
className={classNames('form-control', styles.customConfig)}
debounceTimeout={DEBOUNCE_TIMEOUT}
disabled={installMethod !== 'customConfig'}
element='textarea'

View File

@@ -7,7 +7,7 @@ import pick from 'lodash/pick'
import React, { cloneElement, Component } from 'react'
import { NavLink, NavTabs } from 'nav'
import { Text } from 'editable'
import { editPool } from 'xo'
import { editPool, isSrWritable } from 'xo'
import { Container, Row, Col } from 'grid'
import {
connectStore,
@@ -63,7 +63,7 @@ import TabStorage from './tab-storage'
const getPoolSrs = createGetObjectsOfType('SR').filter(
createSelector(
getPool,
({ id }) => sr => sr.content_type === 'user' && sr.$pool === id
({ id }) => sr => isSrWritable(sr) && sr.$pool === id
)
).sort()

View File

@@ -29,7 +29,7 @@ export default ({
{map(srs, sr => {
return <tr key={sr.id}>
<td>
<Text value={sr.name_label} onChange={value => editSr(sr, { name_label: value })} />
<Text value={sr.name_label} onChange={nameLabel => editSr(sr, { nameLabel })} />
</td>
<td>{sr.SR_type}</td>
<td>{formatSize(sr.size)}</td>

View File

@@ -40,7 +40,7 @@ const USER_COLUMNS = [
name: _('userPermissionColumn'),
itemRenderer: user => <Select
clearable={false}
value={user.permission}
value={user.permission || permissions.none.value}
ref='permission'
onChange={permission => editUser(user, {permission: permission.value})}
options={map(permissions)}
@@ -62,13 +62,17 @@ const USER_COLUMNS = [
})
@injectIntl
export default class Users extends Component {
componentWillMount () {
this._selectPermission(permissions.none)
}
_create = () => {
const {email, password} = this.refs
const { permission } = this.state
return createUser(email.value, password.value, permission.value)
.then(() => {
email.value = password.value = ''
this.setState({permission: undefined})
this._selectPermission(permissions.none)
})
}
@@ -94,7 +98,7 @@ export default class Users extends Component {
clearable={false}
ref='permission'
options={map(permissions)}
value={this.state.permission || permissions.none}
value={this.state.permission}
placeholder={intl.formatMessage(messages.selectPermission)}
onChange={this._selectPermission}
required

View File

@@ -1,21 +1,28 @@
import _ from 'intl'
import ActionRowButton from 'action-row-button'
import CenterPanel from 'center-panel'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import keys from 'lodash/keys'
import Link from 'react-router/lib/Link'
import map from 'lodash/map'
import Page from '../page'
import React from 'react'
import SingleLineRow from 'single-line-row'
import { ButtonGroup } from 'react-bootstrap-4/lib'
import { Card, CardBlock, CardHeader } from 'card'
import { connectStore } from 'utils'
import { Container, Row, Col } from 'grid'
import {
createGetObject,
createGetObjectsOfType,
createSelector
} from 'selectors'
import {
cancelTask,
destroyTask
} from 'xo'
import Page from '../page'
const HEADER = <Container>
<Row>
@@ -25,16 +32,38 @@ const HEADER = <Container>
</Row>
</Container>
const TASK_ITEM_STYLE = {
// Remove all margin, otherwise it breaks vertical alignment.
margin: 0
}
export const TaskItem = connectStore(() => ({
host: createGetObject((_, props) => props.task.$host)
}))(({ task, host }) => <Row>
}))(({ task, host }) => <SingleLineRow className='m-b-1'>
<Col mediumSize={6}>
{task.name_label} (on <Link to={`/hosts/${host.id}`}>{host.name_label}</Link>)
{' ' + Math.round(task.progress * 100)}%
</Col>
<Col mediumSize={6}>
<progress className='progress' value={task.progress * 100} max='100'></progress>
<Col mediumSize={4}>
<progress style={TASK_ITEM_STYLE} className='progress' value={task.progress * 100} max='100'></progress>
</Col>
</Row>)
<Col mediumSize={2}>
<ButtonGroup>
<ActionRowButton
btnStyle='default'
handler={cancelTask}
handlerParam={task}
icon='task-cancel'
/>
<ActionRowButton
btnStyle='default'
handler={destroyTask}
handlerParam={task}
icon='task-destroy'
/>
</ButtonGroup>
</Col>
</SingleLineRow>)
export default connectStore(() => {
const getPendingTasksByPool = createGetObjectsOfType('task').filter(
@@ -71,17 +100,19 @@ export default connectStore(() => {
}
return <Page header={HEADER}>
<Card>
<Container>
{map(pools, pool =>
<span>
<CardHeader key={pool.id}><Link to={`/pools/${pool.id}`}>{pool.name_label}</Link></CardHeader>
<CardBlock>
{map(pendingTasksByPool[pool.id], task =>
<TaskItem key={task.id} task={task} />
)}
</CardBlock>
</span>
<Row>
<Card>
<CardHeader key={pool.id}><Link to={`/pools/${pool.id}`}>{pool.name_label}</Link></CardHeader>
<CardBlock>
{map(pendingTasksByPool[pool.id], task =>
<TaskItem key={task.id} task={task} />
)}
</CardBlock>
</Card>
</Row>
)}
</Card>
</Container>
</Page>
})

View File

@@ -1,30 +1,101 @@
import React, { Component } from 'react'
import _, { messages } from 'intl'
import ActionButton from 'action-button'
import BaseComponent from 'base-component'
import Icon from 'icon'
import React from 'react'
import { alert } from 'modal'
import { autobind, connectStore } from 'utils'
import { changePassword } from 'xo'
import { Container, Row, Col } from 'grid'
import { getLang, getUser } from 'selectors'
import { injectIntl } from 'react-intl'
import Page from '../page'
const HEADER = <Container>
<Row>
<Col>
<h2><Icon icon='user' /> {_('userPage')}</h2>
</Col>
</Row>
</Container>
@connectStore({
lang: getLang,
user: getUser
})
export default class User extends Component {
@injectIntl
export default class User extends BaseComponent {
@autobind
handleSelectLang (event) {
this.props.selectLang(event.target.value)
}
_handleSavePassword = () => {
const { oldPassword, newPassword, confirmPassword } = this.state
if (newPassword !== confirmPassword) {
return alert(_('confirmationPasswordError'), _('confirmationPasswordErrorBody'))
}
return changePassword(oldPassword, newPassword).then(() => this.setState({
oldPassword: undefined,
newPassword: undefined,
confirmPassword: undefined
}))
}
_handleOldPasswordChange = event => this.setState({ oldPassword: event.target.value })
_handleNewPasswordChange = event => this.setState({ newPassword: event.target.value })
_handleConfirmPasswordChange = event => this.setState({ confirmPassword: event.target.value })
render () {
const { formatMessage } = this.props.intl
const {
lang,
user
} = this.props
const {
confirmPassword,
newPassword,
oldPassword
} = this.state
return <div>
{user && user.email}
<select className='form-control' onChange={this.handleSelectLang} value={lang} style={{width: '10em'}}>
<option value='en'>English</option>
<option value='fr'>Français</option>
<option value='he'>עברי</option>
<option value='pt'>Português</option>
</select>
</div>
return <Page header={HEADER}>
<Container>
<Row>
<Col smallSize={2}><strong>{_('username')}</strong></Col>
<Col smallSize={10}>
{user && user.email}
</Col>
</Row>
<br />
<Row>
<Col smallSize={2}><strong>{_('password')}</strong></Col>
<Col smallSize={10}>
<form className='form-inline' id='changePassword'>
<input type='password' onChange={this._handleOldPasswordChange} value={oldPassword} placeholder={formatMessage(messages.oldPasswordPlaceholder)} className='form-control' required />
{' '}
<input type='password' onChange={this._handleNewPasswordChange} value={newPassword} placeholder={formatMessage(messages.newPasswordPlaceholder)} className='form-control' required />
{' '}
<input type='password' onChange={this._handleConfirmPasswordChange} value={confirmPassword} placeholder={formatMessage(messages.confirmPasswordPlaceholder)} className='form-control' required />
{' '}
<ActionButton icon='save' form='changePassword' btnStyle='primary' handler={this._handleSavePassword}>
{_('changePasswordOk')}
</ActionButton>
</form>
</Col>
</Row>
<br />
<Row>
<Col smallSize={2}><strong>{_('language')}</strong></Col>
<Col smallSize={10}>
<select className='form-control' onChange={this.handleSelectLang} value={lang} style={{width: '10em'}}>
<option value='en'>English</option>
<option value='fr'>Français</option>
<option value='he'>עברי</option>
<option value='pt'>Português</option>
</select>
</Col>
</Row>
</Container>
</Page>
}
}

View File

@@ -9,7 +9,7 @@ import filter from 'lodash/filter'
import map from 'lodash/map'
import Upgrade from 'xoa-upgrade'
import { Container, Col, Row } from 'grid'
import { importVms } from 'xo'
import { importVms, isSrWritable } from 'xo'
import {
SelectPool,
SelectSr
@@ -52,7 +52,7 @@ export default class Import extends Component {
_handleSelectedPool = pool => {
const srPredicate = pool !== ''
? sr => sr.$pool === pool.id && sr.content_type === 'user'
? sr => sr.$pool === pool.id && isSrWritable(sr)
: undefined
this.setState({

View File

@@ -28,6 +28,7 @@ import {
deleteVdi,
disconnectVbd,
editVdi,
isSrWritable,
migrateVdi,
setBootableVbd,
setVmBootOrder
@@ -87,7 +88,7 @@ class NewDisk extends Component {
const { vm } = this.props
return vm && vm.$pool
},
poolId => sr => sr.$pool === poolId && sr.content_type === 'user'
poolId => sr => sr.$pool === poolId && isSrWritable(sr)
)
render () {
@@ -135,7 +136,7 @@ class AttachDisk extends Component {
const { vm } = this.props
return vm && vm.$pool
},
poolId => sr => sr.$pool === poolId && sr.content_type === 'user'
poolId => sr => sr.$pool === poolId && isSrWritable(sr)
)
_selectVdi = vdi => this.setState({vdi})
@@ -408,7 +409,7 @@ export default class TabDisks extends Component {
<XoSelect
onChange={sr => migrateVdi(vdi, sr)}
xoType='SR'
predicate={sr => (sr.$pool === vm.$pool) && (sr.content_type === 'user')}
predicate={sr => sr.$pool === vm.$pool && isSrWritable(sr)}
labelProp='name_label'
value={sr}
useLongClick