feat(xo-web/setting/remotes): ability to change the type of a remote (#3207)

This commit is contained in:
badrAZ 2018-07-25 14:24:25 +02:00 committed by Pierre Donias
parent 0ed1df3af6
commit b95fc86667
4 changed files with 429 additions and 322 deletions

View File

@ -8,6 +8,7 @@
- Search syntax support wildcard (`*`) and regular expressions [#3190](https://github.com/vatesfr/xen-orchestra/issues/3190) (PRs [#3198](https://github.com/vatesfr/xen-orchestra/pull/3198) & [#3199](https://github.com/vatesfr/xen-orchestra/pull/3199)) - Search syntax support wildcard (`*`) and regular expressions [#3190](https://github.com/vatesfr/xen-orchestra/issues/3190) (PRs [#3198](https://github.com/vatesfr/xen-orchestra/pull/3198) & [#3199](https://github.com/vatesfr/xen-orchestra/pull/3199))
- Import VDI content [#2432](https://github.com/vatesfr/xen-orchestra/issues/2432) (PR [#3216](https://github.com/vatesfr/xen-orchestra/pull/3216)) - Import VDI content [#2432](https://github.com/vatesfr/xen-orchestra/issues/2432) (PR [#3216](https://github.com/vatesfr/xen-orchestra/pull/3216))
- [Backup NG form] Ability to edit a schedule's name [#2711](https://github.com/vatesfr/xen-orchestra/issues/2711) [#3071](https://github.com/vatesfr/xen-orchestra/issues/3071) (PR [#3143](https://github.com/vatesfr/xen-orchestra/pull/3143)) - [Backup NG form] Ability to edit a schedule's name [#2711](https://github.com/vatesfr/xen-orchestra/issues/2711) [#3071](https://github.com/vatesfr/xen-orchestra/issues/3071) (PR [#3143](https://github.com/vatesfr/xen-orchestra/pull/3143))
- [Remotes] Ability to change the type of a remote [#2423](https://github.com/vatesfr/xen-orchestra/issues/2423) (PR [#3207](https://github.com/vatesfr/xen-orchestra/pull/3207))
### Bug fixes ### Bug fixes

View File

@ -0,0 +1,15 @@
// TODO: remove these functions once the PR: https://github.com/julien-f/freactal/pull/5 has been merged
// It only supports native inputs
export const linkState = (_, { target }) => state => ({
...state,
[target.name]:
target.nodeName.toLowerCase() === 'input' &&
target.type.toLowerCase() === 'checkbox'
? target.checked
: target.value,
})
export const toggleState = (_, { target: { name } }) => state => ({
...state,
[name]: !state[name],
})

View File

@ -1,22 +1,18 @@
import _, { messages } from 'intl' import _, { messages } from 'intl'
import ActionButton from 'action-button'
import Component from 'base-component'
import Icon from 'icon' import Icon from 'icon'
import React from 'react' import React from 'react'
import SortedTable from 'sorted-table' import SortedTable from 'sorted-table'
import StateButton from 'state-button' import StateButton from 'state-button'
import Tooltip from 'tooltip' import Tooltip from 'tooltip'
import { addSubscriptions } from 'utils' import { addSubscriptions, generateRandomId } from 'utils'
import { alert, confirm } from 'modal' import { alert } from 'modal'
import { error } from 'notification'
import { format, parse } from 'xo-remote-parser' import { format, parse } from 'xo-remote-parser'
import { groupBy, map, isEmpty, some } from 'lodash' import { groupBy, map, isEmpty } from 'lodash'
import { injectIntl } from 'react-intl' import { injectIntl } from 'react-intl'
import { Number as InputNumber } from 'form' import { injectState, provideState } from '@julien-f/freactal'
import { Number, Password, Text } from 'editable' import { Number, Password, Text } from 'editable'
import { import {
createRemote,
deleteRemote, deleteRemote,
deleteRemotes, deleteRemotes,
disableRemote, disableRemote,
@ -26,11 +22,8 @@ import {
testRemote, testRemote,
} from 'xo' } from 'xo'
const remoteTypes = { import Remote from './remote'
file: 'remoteTypeLocal',
nfs: 'remoteTypeNfs',
smb: 'remoteTypeSmb',
}
const _changeUrlElement = (value, { remote, element }) => const _changeUrlElement = (value, { remote, element }) =>
editRemote(remote, { editRemote(remote, {
url: format({ ...remote, [element]: value === null ? undefined : value }), url: format({ ...remote, [element]: value === null ? undefined : value }),
@ -230,6 +223,12 @@ const INDIVIDUAL_ACTIONS = [
label: _('remoteTestTip'), label: _('remoteTestTip'),
level: 'primary', level: 'primary',
}, },
{
handler: (remote, { editRemote }) => editRemote(remote),
icon: 'edit',
label: _('formEdit'),
level: 'primary',
},
{ {
handler: deleteRemote, handler: deleteRemote,
icon: 'delete', icon: 'delete',
@ -242,317 +241,94 @@ const FILTERS = {
filterRemotesOnlyDisconnected: '!enabled?', filterRemotesOnlyDisconnected: '!enabled?',
} }
@addSubscriptions({ export default [
remotes: cb => addSubscriptions({
subscribeRemotes(remotes => { remotes: cb =>
cb( subscribeRemotes(remotes => {
groupBy( cb(
map(remotes, remote => { groupBy(
try { map(remotes, remote => {
return { try {
...remote, return {
...parse(remote.url), ...remote,
...parse(remote.url),
}
} catch (err) {
console.error('Remote parsing error:', remote, '\n', err)
} }
} catch (err) { }).filter(r => r !== undefined),
console.error('Remote parsing error:', remote, '\n', err) 'type'
} )
}).filter(r => r !== undefined),
'type'
) )
) }),
}),
injectIntl,
provideState({
initialState: () => ({
formId: generateRandomId(),
remote: undefined,
}), }),
}) effects: {
@injectIntl reset: () => () => ({
export default class Remotes extends Component { formId: generateRandomId(),
constructor (props) { remote: undefined,
super(props) }),
this.state = { editRemote: (_, remote) => () => ({
domain: '', remote,
host: '', }),
name: '', },
password: '', }),
path: '', injectState,
port: undefined, ({ state, effects, remotes = {}, intl: { formatMessage } }) => (
type: 'nfs', <div>
username: '', {!isEmpty(remotes.file) && (
} <div>
} <h2>{_('remoteTypeLocal')}</h2>
<SortedTable
collection={remotes.file}
columns={COLUMNS_LOCAL_REMOTE}
data-editRemote={effects.editRemote}
data-formatMessage={formatMessage}
filters={FILTERS}
groupedActions={GROUPED_ACTIONS}
individualActions={INDIVIDUAL_ACTIONS}
stateUrlParam='l'
/>
</div>
)}
_checkNameExists = () => {!isEmpty(remotes.nfs) && (
some(this.props.remotes, values => some(values, ['name', this.state.name])) <div>
? alert( <h2>{_('remoteTypeNfs')}</h2>
<span> <SortedTable
<Icon icon='error' /> {_('remoteTestName')} collection={remotes.nfs}
</span>, columns={COLUMNS_NFS_REMOTE}
<p>{_('remoteTestNameFailure')}</p> data-editRemote={effects.editRemote}
) data-formatMessage={formatMessage}
: this._createRemote() filters={FILTERS}
groupedActions={GROUPED_ACTIONS}
individualActions={INDIVIDUAL_ACTIONS}
stateUrlParam='nfs'
/>
</div>
)}
_createRemote = async () => { {!isEmpty(remotes.smb) && (
const { <div>
domain, <h2>{_('remoteTypeSmb')}</h2>
host, <SortedTable
name, collection={remotes.smb}
password, columns={COLUMNS_SMB_REMOTE}
path, data-editRemote={effects.editRemote}
port, data-formatMessage={formatMessage}
type, filters={FILTERS}
username, groupedActions={GROUPED_ACTIONS}
} = this.state individualActions={INDIVIDUAL_ACTIONS}
stateUrlParam='smb'
const urlParams = { />
host, </div>
path, )}
port, <Remote formatMessage={formatMessage} key={state.formId} />
type, </div>
} ),
username && (urlParams.username = username) ].reduceRight((value, decorator) => decorator(value))
password && (urlParams.password = password)
domain && (urlParams.domain = domain)
if (type === 'file') {
await confirm({
title: _('localRemoteWarningTitle'),
body: _('localRemoteWarningMessage'),
})
}
const url = format(urlParams)
return createRemote(name, url).then(
() => {
this.setState({
domain: '',
host: '',
name: '',
password: '',
path: '',
port: undefined,
type: 'nfs',
username: '',
})
},
err => error('Create Remote', err.message || String(err))
)
}
render () {
const {
intl: { formatMessage },
remotes = {},
} = this.props
const {
domain,
host,
name,
password,
path,
port,
type,
username,
} = this.state
return (
<div>
{!isEmpty(remotes.file) && (
<div>
<h2>{_('remoteTypeLocal')}</h2>
<SortedTable
collection={remotes.file}
columns={COLUMNS_LOCAL_REMOTE}
data-formatMessage={formatMessage}
filters={FILTERS}
groupedActions={GROUPED_ACTIONS}
individualActions={INDIVIDUAL_ACTIONS}
stateUrlParam='l'
/>
</div>
)}
{!isEmpty(remotes.nfs) && (
<div>
<h2>{_('remoteTypeNfs')}</h2>
<SortedTable
collection={remotes.nfs}
columns={COLUMNS_NFS_REMOTE}
data-formatMessage={formatMessage}
filters={FILTERS}
groupedActions={GROUPED_ACTIONS}
individualActions={INDIVIDUAL_ACTIONS}
stateUrlParam='nfs'
/>
</div>
)}
{!isEmpty(remotes.smb) && (
<div>
<h2>{_('remoteTypeSmb')}</h2>
<SortedTable
collection={remotes.smb}
columns={COLUMNS_SMB_REMOTE}
data-formatMessage={formatMessage}
filters={FILTERS}
groupedActions={GROUPED_ACTIONS}
individualActions={INDIVIDUAL_ACTIONS}
stateUrlParam='smb'
/>
</div>
)}
<h2>{_('newRemote')}</h2>
<form id='newRemoteForm'>
<div className='form-group'>
<label htmlFor='newRemoteType'>{_('remoteType')}</label>
<select
id='newRemoteType'
className='form-control'
onChange={this.linkState('type')}
required
value={type}
>
{map(remoteTypes, (label, key) =>
_({ key }, label, message => (
<option value={key}>{message}</option>
))
)}
</select>
{type === 'smb' && (
<em className='text-warning'>{_('remoteSmbWarningMessage')}</em>
)}
</div>
<div className='form-group'>
<input
className='form-control'
onChange={this.linkState('name')}
placeholder={formatMessage(messages.remoteMyNamePlaceHolder)}
required
type='text'
value={name}
/>
</div>
{type === 'file' && (
<fieldset className='form-group'>
<div className='input-group'>
<span className='input-group-addon'>/</span>
<input
className='form-control'
onChange={this.linkState('path')}
pattern='^(([^/]+)+(/[^/]+)*)?$'
placeholder={formatMessage(
messages.remoteLocalPlaceHolderPath
)}
type='text'
value={path}
/>
</div>
</fieldset>
)}
{type === 'nfs' && (
<fieldset>
<div className='form-group'>
<input
className='form-control'
onChange={this.linkState('host')}
placeholder={formatMessage(messages.remoteNfsPlaceHolderHost)}
type='text'
value={host}
required
/>
<br />
<InputNumber
onChange={this.linkState('port')}
placeholder={formatMessage(messages.remoteNfsPlaceHolderPort)}
value={port}
/>
</div>
<div className='input-group form-group'>
<span className='input-group-addon'>/</span>
<input
className='form-control'
onChange={this.linkState('path')}
pattern='^(([^/]+)+(/[^/]+)*)?$'
placeholder={formatMessage(messages.remoteNfsPlaceHolderPath)}
type='text'
value={path}
/>
</div>
</fieldset>
)}
{type === 'smb' && (
<fieldset>
<div className='input-group form-group'>
<span className='input-group-addon'>\\</span>
<input
className='form-control'
onChange={this.linkState('host')}
pattern='^([^\\/]+)\\([^\\/]+)$'
placeholder={formatMessage(
messages.remoteSmbPlaceHolderAddressShare
)}
type='text'
value={host}
required
/>
<span className='input-group-addon'>\</span>
<input
className='form-control'
onChange={this.linkState('path')}
pattern='^(([^\\/]+)+(\\[^\\/]+)*)?$'
placeholder={formatMessage(
messages.remoteSmbPlaceHolderRemotePath
)}
type='text'
value={path}
/>
</div>
<div className='form-group'>
<input
className='form-control'
onChange={this.linkState('username')}
placeholder={formatMessage(
messages.remoteSmbPlaceHolderUsername
)}
type='text'
value={username}
/>
</div>
<div className='form-group'>
<input
className='form-control'
onChange={this.linkState('password')}
placeholder={formatMessage(
messages.remoteSmbPlaceHolderPassword
)}
type='text'
value={password}
/>
</div>
<div className='form-group'>
<input
className='form-control'
onChange={this.linkState('domain')}
placeholder={formatMessage(
messages.remoteSmbPlaceHolderDomain
)}
required
type='text'
value={domain}
/>
</div>
</fieldset>
)}
<div className='form-group'>
<ActionButton
type='submit'
form='newRemoteForm'
icon='save'
btnStyle='primary'
handler={this._checkNameExists}
>
{_('savePluginConfiguration')}
</ActionButton>
</div>
</form>
</div>
)
}
}

View File

@ -0,0 +1,315 @@
import _, { messages } from 'intl'
import ActionButton from 'action-button'
import Icon from 'icon'
import React from 'react'
import { addSubscriptions, generateRandomId } from 'utils'
import { alert, confirm } from 'modal'
import { createRemote, editRemote, subscribeRemotes } from 'xo'
import { error } from 'notification'
import { format } from 'xo-remote-parser'
import { injectState, provideState } from '@julien-f/freactal'
import { linkState } from 'freactal-utils'
import { map, some, trimStart } from 'lodash'
import { Number } from 'form'
const remoteTypes = {
file: 'remoteTypeLocal',
nfs: 'remoteTypeNfs',
smb: 'remoteTypeSmb',
}
export default [
addSubscriptions({
remotes: subscribeRemotes,
}),
provideState({
initialState: () => ({
domain: undefined,
host: undefined,
inputTypeId: generateRandomId(),
name: undefined,
password: undefined,
path: undefined,
port: undefined,
type: undefined,
username: undefined,
}),
effects: {
linkState,
setPort: (_, port) => state => ({
port: port === undefined && state.remote !== undefined ? '' : port,
}),
editRemote: ({ reset }) => state => {
const {
remote,
domain = remote.domain,
host = remote.host,
name,
password = remote.password,
path = remote.path,
port = remote.port,
type = remote.type,
username = remote.username,
} = state
return editRemote(remote, {
name,
url: format({
domain,
host,
password,
path,
port: port || undefined,
type,
username,
}),
}).then(reset)
},
createRemote: ({ reset }) => async (state, { remotes }) => {
if (some(remotes, { name: state.name })) {
return alert(
<span>
<Icon icon='error' /> {_('remoteTestName')}
</span>,
<p>{_('remoteTestNameFailure')}</p>
)
}
const {
domain,
host,
name,
password,
path,
port,
type = 'nfs',
username,
} = state
const urlParams = {
host,
path,
port,
type,
}
username && (urlParams.username = username)
password && (urlParams.password = password)
domain && (urlParams.domain = domain)
if (type === 'file') {
await confirm({
title: _('localRemoteWarningTitle'),
body: _('localRemoteWarningMessage'),
})
}
const url = format(urlParams)
return createRemote(name, url)
.then(reset)
.catch(err => error('Create Remote', err.message || String(err)))
},
},
computed: {
parsedPath: ({ remote }) => remote && trimStart(remote.path, '/'),
},
}),
injectState,
({ state, effects, formatMessage }) => {
const {
remote = {},
domain = remote.domain || '',
host = remote.host || '',
name = remote.name || '',
password = remote.password || '',
parsedPath,
path = parsedPath || '',
port = remote.port,
type = remote.type || 'nfs',
username = remote.username || '',
} = state
return (
<div>
<h2>{_('newRemote')}</h2>
<form id={state.formId}>
<div className='form-group'>
<label htmlFor={state.inputTypeId}>{_('remoteType')}</label>
<select
className='form-control'
id={state.inputTypeId}
name='type'
onChange={effects.linkState}
required
value={type}
>
{map(remoteTypes, (label, key) =>
_({ key }, label, message => (
<option value={key}>{message}</option>
))
)}
</select>
{type === 'smb' && (
<em className='text-warning'>{_('remoteSmbWarningMessage')}</em>
)}
</div>
<div className='form-group'>
<input
className='form-control'
name='name'
onChange={effects.linkState}
placeholder={formatMessage(messages.remoteMyNamePlaceHolder)}
required
type='text'
value={name}
/>
</div>
{type === 'file' && (
<fieldset className='form-group'>
<div className='input-group'>
<span className='input-group-addon'>/</span>
<input
className='form-control'
name='path'
onChange={effects.linkState}
pattern='^(([^/]+)+(/[^/]+)*)?$'
placeholder={formatMessage(
messages.remoteLocalPlaceHolderPath
)}
required
type='text'
value={path}
/>
</div>
</fieldset>
)}
{type === 'nfs' && (
<fieldset>
<div className='form-group'>
<input
className='form-control'
name='host'
onChange={effects.linkState}
placeholder={formatMessage(messages.remoteNfsPlaceHolderHost)}
required
type='text'
value={host}
/>
<br />
<Number
onChange={effects.setPort}
placeholder={formatMessage(messages.remoteNfsPlaceHolderPort)}
value={port}
/>
</div>
<div className='input-group form-group'>
<span className='input-group-addon'>/</span>
<input
className='form-control'
name='path'
onChange={effects.linkState}
pattern='^(([^/]+)+(/[^/]+)*)?$'
placeholder={formatMessage(messages.remoteNfsPlaceHolderPath)}
required
type='text'
value={path}
/>
</div>
</fieldset>
)}
{type === 'smb' && (
<fieldset>
<div className='input-group form-group'>
<span className='input-group-addon'>\\</span>
<input
className='form-control'
name='host'
onChange={effects.linkState}
pattern='^([^\\/]+)\\([^\\/]+)$'
placeholder={formatMessage(
messages.remoteSmbPlaceHolderAddressShare
)}
required
type='text'
value={host}
/>
<span className='input-group-addon'>\</span>
<input
className='form-control'
name='path'
onChange={effects.linkState}
pattern='^(([^\\/]+)+(\\[^\\/]+)*)?$'
placeholder={formatMessage(
messages.remoteSmbPlaceHolderRemotePath
)}
required
type='text'
value={path}
/>
</div>
<div className='form-group'>
<input
className='form-control'
name='username'
onChange={effects.linkState}
placeholder={formatMessage(
messages.remoteSmbPlaceHolderUsername
)}
required
type='text'
value={username}
/>
</div>
<div className='form-group'>
<input
className='form-control'
name='password'
onChange={effects.linkState}
placeholder={formatMessage(
messages.remoteSmbPlaceHolderPassword
)}
required
type='text'
value={password}
/>
</div>
<div className='form-group'>
<input
className='form-control'
onChange={effects.linkState}
name='domain'
placeholder={formatMessage(
messages.remoteSmbPlaceHolderDomain
)}
required
type='text'
value={domain}
/>
</div>
</fieldset>
)}
<div className='form-group'>
<ActionButton
btnStyle='primary'
form={state.formId}
handler={
state.remote === undefined
? effects.createRemote
: effects.editRemote
}
icon='save'
type='submit'
>
{_('savePluginConfiguration')}
</ActionButton>
<ActionButton
className='pull-right'
handler={effects.reset}
icon='reset'
type='reset'
>
{_('formReset')}
</ActionButton>
</div>
</form>
</div>
)
},
].reduceRight((value, decorator) => decorator(value))