diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md
index 9f90de839..c8209c8cc 100644
--- a/CHANGELOG.unreleased.md
+++ b/CHANGELOG.unreleased.md
@@ -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
diff --git a/packages/xo-web/src/common/intl/messages.js b/packages/xo-web/src/common/intl/messages.js
index 9a3024fec..db5cea6ba 100644
--- a/packages/xo-web/src/common/intl/messages.js
+++ b/packages/xo-web/src/common/intl/messages.js
@@ -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',
diff --git a/packages/xo-web/src/common/xo/create-bonded-network-modal/index.js b/packages/xo-web/src/common/xo/create-bonded-network-modal/index.js
deleted file mode 100644
index e701caefb..000000000
--- a/packages/xo-web/src/common/xo/create-bonded-network-modal/index.js
+++ /dev/null
@@ -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 (
-
-
- {_('newNetworkInterface')}
-
-
-
-
-
-
- {_('newNetworkName')}
-
-
-
-
-
-
- {_('newNetworkDescription')}
-
-
-
-
-
-
- {_('newNetworkMtu')}
-
-
-
-
-
-
- {_('newNetworkBondMode')}
-
-
-
-
-
- )
- }
-}
-export default injectIntl(CreateBondedNetworkModalBody, { withRef: true })
diff --git a/packages/xo-web/src/common/xo/create-network-modal/index.js b/packages/xo-web/src/common/xo/create-network-modal/index.js
deleted file mode 100644
index 1a8e75fa1..000000000
--- a/packages/xo-web/src/common/xo/create-network-modal/index.js
+++ /dev/null
@@ -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 (
-
-
- {_('newNetworkInterface')}
-
-
-
-
-
-
- {_('newNetworkName')}
-
-
-
-
-
-
- {_('newNetworkDescription')}
-
-
-
-
-
-
- {_('newNetworkVlan')}
-
-
-
-
-
-
- {_('newNetworkMtu')}
-
-
-
-
-
- )
- }
-}
-export default injectIntl(CreateNetworkModalBody, { withRef: true })
diff --git a/packages/xo-web/src/common/xo/index.js b/packages/xo-web/src/common/xo/index.js
index 3efbd934b..5464407da 100644
--- a/packages/xo-web/src/common/xo/index.js
+++ b/packages/xo-web/src/common/xo/index.js
@@ -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: ,
- }).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: ,
- }).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({
diff --git a/packages/xo-web/src/icons.scss b/packages/xo-web/src/icons.scss
index d564512a5..a74270840 100644
--- a/packages/xo-web/src/icons.scss
+++ b/packages/xo-web/src/icons.scss
@@ -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;
diff --git a/packages/xo-web/src/xo-app/menu/index.js b/packages/xo-web/src/xo-app/menu/index.js
index fa6e28b84..e468153b4 100644
--- a/packages/xo-web/src/xo-app/menu/index.js
+++ b/packages/xo-web/src/xo-app/menu/index.js
@@ -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',
diff --git a/packages/xo-web/src/xo-app/new/index.js b/packages/xo-web/src/xo-app/new/index.js
index 3c5568831..588cb2b59 100644
--- a/packages/xo-web/src/xo-app/new/index.js
+++ b/packages/xo-web/src/xo-app/new/index.js
@@ -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)
diff --git a/packages/xo-web/src/xo-app/new/network/index.css b/packages/xo-web/src/xo-app/new/network/index.css
new file mode 100644
index 000000000..560e7fe93
--- /dev/null
+++ b/packages/xo-web/src/xo-app/new/network/index.css
@@ -0,0 +1,5 @@
+.inlineSelect {
+ display: inline-block;
+ font-size: 1rem;
+ width: 20em;
+}
diff --git a/packages/xo-web/src/xo-app/new/network/index.js b/packages/xo-web/src/xo-app/new/network/index.js
new file mode 100644
index 000000000..aa3acb67b
--- /dev/null
+++ b/packages/xo-web/src/xo-app/new/network/index.js
@@ -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 (
+
+ {isPoolAdmin
+ ? _('createNewNetworkOn', {
+ select: (
+
+
+
+ ),
+ })
+ : _('createNewNetworkNoPermission')}
+
+ )
+ }
+
+ render() {
+ const { state, effects, intl, pool } = this.props
+ const {
+ bonded,
+ bondMode,
+ description,
+ modeOptions,
+ mtu,
+ name,
+ pif,
+ pifPredicate,
+ pifs,
+ vlan,
+ } = state
+ const { formatMessage } = intl
+ return (
+
+ {pool !== undefined && (
+
+ )}
+
+ )
+ }
+ },
+])
+export { NewNetwork as default }
diff --git a/packages/xo-web/src/xo-app/pool/tab-network.js b/packages/xo-web/src/xo-app/pool/tab-network.js
index daf6f1541..856842883 100644
--- a/packages/xo-web/src/xo-app/pool/tab-network.js
+++ b/packages/xo-web/src/xo-app/pool/tab-network.js
@@ -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 {
-
-