feat(xo-web/new-network): dedicated view (#3906)

Fixes #3895
This commit is contained in:
Rajaa.BARHTAOUI 2019-02-21 11:43:40 +01:00 committed by Pierre Donias
parent dc34f3478d
commit 7a2a88b7ad
11 changed files with 280 additions and 246 deletions

View File

@ -5,6 +5,7 @@
- [VM migration] Display same-pool hosts first in the selector [#3262](https://github.com/vatesfr/xen-orchestra/issues/3262) (PR [#3890](https://github.com/vatesfr/xen-orchestra/pull/3890))
- [Home/VM] Sort VM by start time [#3955](https://github.com/vatesfr/xen-orchestra/issues/3955) (PR [#3970](https://github.com/vatesfr/xen-orchestra/pull/3970))
- [Editable fields] Unfocusing (clicking outside) submits the change instead of canceling (PR [#3980](https://github.com/vatesfr/xen-orchestra/pull/3980))
- [Network] Dedicated page for network creation [#3895](https://github.com/vatesfr/xen-orchestra/issues/3895) (PR [#3906](https://github.com/vatesfr/xen-orchestra/pull/3906))
### Bug fixes

View File

@ -101,6 +101,7 @@ const messages = {
newMenu: 'New',
taskMenu: 'Tasks',
taskPage: 'Tasks',
newNetworkPage: 'Network',
newVmPage: 'VM',
newSrPage: 'Storage',
newServerPage: 'Server',
@ -563,6 +564,10 @@ const messages = {
newSrNfsOptions: 'Comma delimited NFS options',
reattachNewSrTooltip: 'Reattach SR',
// ------ New Newtork -----
createNewNetworkNoPermission: 'You have no permission to create a network',
createNewNetworkOn: 'Create a new network on {select}',
// ----- Acls, Users, Groups ------
subjectName: 'Users/Groups',
objectName: 'Object',
@ -1690,7 +1695,6 @@ const messages = {
// ----- Network -----
newNetworkCreate: 'Create network',
newBondedNetworkCreate: 'Create bonded network',
newNetworkInterface: 'Interface',
newNetworkName: 'Name',
newNetworkDescription: 'Description',
@ -1698,13 +1702,14 @@ const messages = {
newNetworkDefaultVlan: 'No VLAN if empty',
newNetworkMtu: 'MTU',
newNetworkDefaultMtu: 'Default: 1500',
newNetworkNoNameErrorTitle: 'Name required',
newNetworkNoNameErrorMessage: 'A name is required to create a network',
newNetworkBondMode: 'Bond mode',
newNetworkInfo: 'Info',
newNetworkType: 'Type',
deleteNetwork: 'Delete network',
deleteNetworkConfirm: 'Are you sure you want to delete this network?',
networkInUse: 'This network is currently in use',
pillBonded: 'Bonded',
bondedNetwork: 'Bonded network',
// ----- Add host -----
addHostSelectHost: 'Host',

View File

@ -1,113 +0,0 @@
import Component from 'base-component'
import map from 'lodash/map'
import React from 'react'
import { createGetObject, createSelector } from 'selectors'
import { getBondModes } from 'xo'
import { injectIntl } from 'react-intl'
import _, { messages } from '../../intl'
import { Col } from '../../grid'
import { connectStore } from '../../utils'
import { SelectPif } from '../../select-objects'
import SingleLineRow from '../../single-line-row'
@connectStore(
() => ({
poolMaster: createSelector(
createGetObject((_, props) => props.pool),
pool => pool.master
),
}),
{ withRef: true }
)
class CreateBondedNetworkModalBody extends Component {
componentWillMount() {
getBondModes().then(bondModes =>
this.setState({ bondModes, bondMode: bondModes[0] })
)
}
_getPifPredicate = createSelector(
() => this.props.poolMaster,
hostId => pif => pif.$host === hostId && pif.vlan === -1
)
get value() {
const { name, description, pifs, mtu, bondMode } = this.state
return {
pool: this.props.pool,
name,
description,
pifs: map(pifs, pif => pif.id),
mtu,
bondMode,
}
}
render() {
const { formatMessage } = this.props.intl
return (
<div>
<SingleLineRow>
<Col size={6}>{_('newNetworkInterface')}</Col>
<Col size={6}>
<SelectPif
multi
onChange={this.linkState('pifs')}
predicate={this._getPifPredicate()}
/>
</Col>
</SingleLineRow>
&nbsp;
<SingleLineRow>
<Col size={6}>{_('newNetworkName')}</Col>
<Col size={6}>
<input
className='form-control'
onChange={this.linkState('name')}
type='text'
/>
</Col>
</SingleLineRow>
&nbsp;
<SingleLineRow>
<Col size={6}>{_('newNetworkDescription')}</Col>
<Col size={6}>
<input
className='form-control'
onChange={this.linkState('description')}
type='text'
/>
</Col>
</SingleLineRow>
&nbsp;
<SingleLineRow>
<Col size={6}>{_('newNetworkMtu')}</Col>
<Col size={6}>
<input
className='form-control'
onChange={this.linkState('mtu')}
placeholder={formatMessage(messages.newNetworkDefaultMtu)}
type='text'
/>
</Col>
</SingleLineRow>
&nbsp;
<SingleLineRow>
<Col size={6}>{_('newNetworkBondMode')}</Col>
<Col size={6}>
<select
className='form-control'
onChange={this.linkState('bondMode')}
>
{map(this.state.bondModes, mode => (
<option value={mode}>{mode}</option>
))}
</select>
</Col>
</SingleLineRow>
</div>
)
}
}
export default injectIntl(CreateBondedNetworkModalBody, { withRef: true })

View File

@ -1,84 +0,0 @@
import React, { Component } from 'react'
import { injectIntl } from 'react-intl'
import { createSelector } from 'selectors'
import SingleLineRow from '../../single-line-row'
import _, { messages } from '../../intl'
import { SelectPif } from '../../select-objects'
import { Col } from '../../grid'
class CreateNetworkModalBody extends Component {
_getPifPredicate = createSelector(
() => {
const { container } = this.props
return container.type === 'pool' ? container.master : container.id
},
hostId => pif => pif.$host === hostId && pif.vlan === -1
)
get value() {
const { refs } = this
const { container } = this.props
return {
pool: container.$pool,
name: refs.name.value,
description: refs.description.value,
pif: refs.pif.value && refs.pif.value.id,
mtu: refs.mtu.value,
vlan: refs.vlan.value,
}
}
render() {
const { formatMessage } = this.props.intl
return (
<div>
<SingleLineRow>
<Col size={6}>{_('newNetworkInterface')}</Col>
<Col size={6}>
<SelectPif predicate={this._getPifPredicate()} ref='pif' />
</Col>
</SingleLineRow>
&nbsp;
<SingleLineRow>
<Col size={6}>{_('newNetworkName')}</Col>
<Col size={6}>
<input className='form-control' ref='name' type='text' />
</Col>
</SingleLineRow>
&nbsp;
<SingleLineRow>
<Col size={6}>{_('newNetworkDescription')}</Col>
<Col size={6}>
<input className='form-control' ref='description' type='text' />
</Col>
</SingleLineRow>
&nbsp;
<SingleLineRow>
<Col size={6}>{_('newNetworkVlan')}</Col>
<Col size={6}>
<input
className='form-control'
placeholder={formatMessage(messages.newNetworkDefaultVlan)}
ref='vlan'
type='text'
/>
</Col>
</SingleLineRow>
&nbsp;
<SingleLineRow>
<Col size={6}>{_('newNetworkMtu')}</Col>
<Col size={6}>
<input
className='form-control'
placeholder={formatMessage(messages.newNetworkDefaultMtu)}
ref='mtu'
type='text'
/>
</Col>
</SingleLineRow>
</div>
)
}
}
export default injectIntl(CreateNetworkModalBody, { withRef: true })

View File

@ -1627,39 +1627,10 @@ export const setVif = (
export const editNetwork = (network, props) =>
_call('network.set', { ...props, id: resolveId(network) })
import CreateNetworkModalBody from './create-network-modal' // eslint-disable-line import/first
export const createNetwork = container =>
confirm({
icon: 'network',
title: _('newNetworkCreate'),
body: <CreateNetworkModalBody container={container} />,
}).then(params => {
if (!params.name) {
return error(
_('newNetworkNoNameErrorTitle'),
_('newNetworkNoNameErrorMessage')
)
}
return _call('network.create', params)
}, noop)
export const getBondModes = () => _call('network.getBondModes')
import CreateBondedNetworkModalBody from './create-bonded-network-modal' // eslint-disable-line import/first
export const createBondedNetwork = container =>
confirm({
icon: 'network',
title: _('newBondedNetworkCreate'),
body: <CreateBondedNetworkModalBody pool={container.$pool} />,
}).then(params => {
if (!params.name) {
return error(
_('newNetworkNoNameErrorTitle'),
_('newNetworkNoNameErrorMessage')
)
}
return _call('network.createBonded', params)
}, noop)
export const createNetwork = params => _call('network.create', params)
export const createBondedNetwork = params =>
_call('network.createBonded', params)
export const deleteNetwork = network =>
confirm({

View File

@ -854,6 +854,10 @@
@extend .fa;
@extend .fa-database;
}
&-network {
@extend .fa;
@extend .fa-sitemap;
}
&-import {
@extend .fa;
@extend .fa-file-archive-o;
@ -906,6 +910,13 @@
@extend .fa-times;
}
}
// New network
&-new-network {
&-create {
@extend .fa;
@extend .fa-play;
}
}
// OS Icons
&-centos {
@extend .fa;

View File

@ -369,6 +369,11 @@ export default class Menu extends Component {
label: 'newVmPage',
},
isAdmin && { to: '/new/sr', icon: 'menu-new-sr', label: 'newSrPage' },
isPoolAdmin && {
to: '/new/network',
icon: 'menu-new-network',
label: 'newNetworkPage',
},
isAdmin && {
to: '/settings/servers',
icon: 'menu-settings-servers',

View File

@ -1,8 +1,10 @@
import { routes } from 'utils'
import Network from './network'
import Sr from './sr'
const New = routes('vm', {
network: Network,
sr: Sr,
})(({ children }) => children)

View File

@ -0,0 +1,5 @@
.inlineSelect {
display: inline-block;
font-size: 1rem;
width: 20em;
}

View File

@ -0,0 +1,242 @@
import _, { messages } from 'intl'
import ActionButton from 'action-button'
import decorate from 'apply-decorators'
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import Wizard, { Section } from 'wizard'
import { connectStore } from 'utils'
import { createBondedNetwork, createNetwork, getBondModes } from 'xo'
import { createGetObject, getIsPoolAdmin } from 'selectors'
import { injectIntl } from 'react-intl'
import { injectState, provideState } from 'reaclette'
import { linkState } from 'reaclette-utils'
import { map } from 'lodash'
import { Select, Toggle } from 'form'
import { SelectPif, SelectPool } from 'select-objects'
import Page from '../../page'
import styles from './index.css'
const EMPTY = {
bonded: false,
bondMode: undefined,
description: '',
mtu: '',
name: '',
pif: undefined,
pifs: [],
vlan: '',
}
const NewNetwork = decorate([
connectStore(() => ({
isPoolAdmin: getIsPoolAdmin,
pool: createGetObject((_, props) => props.location.query.pool),
})),
injectIntl,
provideState({
initialState: () => ({ ...EMPTY, bondModes: undefined }),
effects: {
initialize: async () => ({ bondModes: await getBondModes() }),
linkState,
onChangeMode: (_, bondMode) => ({ bondMode }),
onChangePif: (_, value) => ({ bonded }) =>
bonded ? { pifs: value } : { pif: value },
reset: () => EMPTY,
toggleBonded: () => ({ bonded }) => ({
...EMPTY,
bonded: !bonded,
}),
},
computed: {
modeOptions: ({ bondModes }) =>
bondModes !== undefined
? bondModes.map(mode => ({
label: mode,
value: mode,
}))
: [],
pifPredicate: (_, { pool }) => pif =>
pif.vlan === -1 && pif.$host === (pool && pool.master),
},
}),
injectState,
class extends Component {
static contextTypes = {
router: PropTypes.object,
}
_create = () => {
const { pool, state } = this.props
const {
bonded,
bondMode,
description,
mtu,
name,
pif,
pifs,
vlan,
} = state
return bonded
? createBondedNetwork({
bondMode: bondMode.value,
description,
mtu,
name,
pifs: map(pifs, 'id'),
pool: pool.id,
vlan,
})
: createNetwork({
description,
mtu,
name,
pif: pif.id,
pool: pool.id,
vlan,
})
}
_selectPool = pool => {
const {
effects,
location: { pathname },
} = this.props
effects.reset()
this.context.router.push({
pathname,
query: pool !== null && { pool: pool.id },
})
}
_renderHeader = () => {
const { isPoolAdmin, pool } = this.props
return (
<h2>
{isPoolAdmin
? _('createNewNetworkOn', {
select: (
<span className={styles.inlineSelect}>
<SelectPool onChange={this._selectPool} value={pool} />
</span>
),
})
: _('createNewNetworkNoPermission')}
</h2>
)
}
render() {
const { state, effects, intl, pool } = this.props
const {
bonded,
bondMode,
description,
modeOptions,
mtu,
name,
pif,
pifPredicate,
pifs,
vlan,
} = state
const { formatMessage } = intl
return (
<Page header={this._renderHeader()}>
{pool !== undefined && (
<form id='networkCreation'>
<Wizard>
<Section icon='network' title='newNetworkType'>
<div>
<Toggle onChange={effects.toggleBonded} value={bonded} />{' '}
<label>{_('bondedNetwork')}</label>
</div>
</Section>
<Section icon='info' title='newNetworkInfo'>
<div className='form-group'>
<label>{_('newNetworkInterface')}</label>
<SelectPif
multi={bonded}
onChange={effects.onChangePif}
predicate={pifPredicate}
required
value={bonded ? pifs : pif}
/>
<label>{_('newNetworkName')}</label>
<input
className='form-control'
name='name'
onChange={effects.linkState}
required
type='text'
value={name}
/>
<label>{_('newNetworkDescription')}</label>
<input
className='form-control'
name='description'
onChange={effects.linkState}
type='text'
value={description}
/>
<label>{_('newNetworkMtu')}</label>
<input
className='form-control'
name='mtu'
onChange={effects.linkState}
placeholder={formatMessage(messages.newNetworkDefaultMtu)}
type='text'
value={mtu}
/>
{bonded ? (
<div>
<label>{_('newNetworkBondMode')}</label>
<Select
onChange={effects.onChangeMode}
options={modeOptions}
required
value={bondMode}
/>
</div>
) : (
<div>
<label>{_('newNetworkVlan')}</label>
<input
className='form-control'
name='vlan'
onChange={effects.linkState}
placeholder={formatMessage(
messages.newNetworkDefaultVlan
)}
type='text'
value={vlan}
/>
</div>
)}
</div>
</Section>
</Wizard>
<div className='form-group pull-right'>
<ActionButton
btnStyle='primary'
className='mr-1'
form='networkCreation'
handler={this._create}
icon='new-network-create'
redirectOnSuccess={`pools/${pool.id}/network`}
>
{_('newNetworkCreate')}
</ActionButton>
<ActionButton handler={effects.reset} icon='reset'>
{_('formReset')}
</ActionButton>
</div>
</form>
)}
</Page>
)
}
},
])
export { NewNetwork as default }

View File

@ -10,10 +10,10 @@ import map from 'lodash/map'
import React, { Component } from 'react'
import some from 'lodash/some'
import SortedTable from 'sorted-table'
import TabButton from 'tab-button'
import Tooltip from 'tooltip'
import { connectStore } from 'utils'
import { Container, Row, Col } from 'grid'
import { TabButtonLink } from 'tab-button'
import { Text, Number } from 'editable'
import { Toggle } from 'form'
import {
@ -24,8 +24,6 @@ import {
} from 'selectors'
import {
connectPif,
createBondedNetwork,
createNetwork,
deleteNetwork,
disconnectPif,
editNetwork,
@ -362,19 +360,10 @@ export default class TabNetworks extends Component {
<Container>
<Row>
<Col className='text-xs-right'>
<TabButton
btnStyle='primary'
handler={createBondedNetwork}
handlerParam={this.props.pool}
icon='add'
labelId='networkCreateBondedButton'
/>
<TabButton
btnStyle='primary'
handler={createNetwork}
handlerParam={this.props.pool}
<TabButtonLink
icon='add'
labelId='networkCreateButton'
to={`new/network?pool=${this.props.pool.id}`}
/>
</Col>
</Row>