feat(xo-web/setting/remotes): ability to change the type of a remote (#3207)
This commit is contained in:
parent
0ed1df3af6
commit
b95fc86667
@ -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))
|
||||
- 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))
|
||||
- [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
|
||||
|
||||
|
15
packages/xo-web/src/common/freactal-utils.js
Normal file
15
packages/xo-web/src/common/freactal-utils.js
Normal 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],
|
||||
})
|
@ -1,22 +1,18 @@
|
||||
import _, { messages } from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import SortedTable from 'sorted-table'
|
||||
import StateButton from 'state-button'
|
||||
import Tooltip from 'tooltip'
|
||||
import { addSubscriptions } from 'utils'
|
||||
import { alert, confirm } from 'modal'
|
||||
import { error } from 'notification'
|
||||
import { addSubscriptions, generateRandomId } from 'utils'
|
||||
import { alert } from 'modal'
|
||||
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 { Number as InputNumber } from 'form'
|
||||
import { injectState, provideState } from '@julien-f/freactal'
|
||||
import { Number, Password, Text } from 'editable'
|
||||
|
||||
import {
|
||||
createRemote,
|
||||
deleteRemote,
|
||||
deleteRemotes,
|
||||
disableRemote,
|
||||
@ -26,11 +22,8 @@ import {
|
||||
testRemote,
|
||||
} from 'xo'
|
||||
|
||||
const remoteTypes = {
|
||||
file: 'remoteTypeLocal',
|
||||
nfs: 'remoteTypeNfs',
|
||||
smb: 'remoteTypeSmb',
|
||||
}
|
||||
import Remote from './remote'
|
||||
|
||||
const _changeUrlElement = (value, { remote, element }) =>
|
||||
editRemote(remote, {
|
||||
url: format({ ...remote, [element]: value === null ? undefined : value }),
|
||||
@ -230,6 +223,12 @@ const INDIVIDUAL_ACTIONS = [
|
||||
label: _('remoteTestTip'),
|
||||
level: 'primary',
|
||||
},
|
||||
{
|
||||
handler: (remote, { editRemote }) => editRemote(remote),
|
||||
icon: 'edit',
|
||||
label: _('formEdit'),
|
||||
level: 'primary',
|
||||
},
|
||||
{
|
||||
handler: deleteRemote,
|
||||
icon: 'delete',
|
||||
@ -242,7 +241,8 @@ const FILTERS = {
|
||||
filterRemotesOnlyDisconnected: '!enabled?',
|
||||
}
|
||||
|
||||
@addSubscriptions({
|
||||
export default [
|
||||
addSubscriptions({
|
||||
remotes: cb =>
|
||||
subscribeRemotes(remotes => {
|
||||
cb(
|
||||
@ -261,97 +261,25 @@ const FILTERS = {
|
||||
)
|
||||
)
|
||||
}),
|
||||
})
|
||||
@injectIntl
|
||||
export default class Remotes extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
domain: '',
|
||||
host: '',
|
||||
name: '',
|
||||
password: '',
|
||||
path: '',
|
||||
port: undefined,
|
||||
type: 'nfs',
|
||||
username: '',
|
||||
}
|
||||
}
|
||||
|
||||
_checkNameExists = () =>
|
||||
some(this.props.remotes, values => some(values, ['name', this.state.name]))
|
||||
? alert(
|
||||
<span>
|
||||
<Icon icon='error' /> {_('remoteTestName')}
|
||||
</span>,
|
||||
<p>{_('remoteTestNameFailure')}</p>
|
||||
)
|
||||
: this._createRemote()
|
||||
|
||||
_createRemote = async () => {
|
||||
const {
|
||||
domain,
|
||||
host,
|
||||
name,
|
||||
password,
|
||||
path,
|
||||
port,
|
||||
type,
|
||||
username,
|
||||
} = this.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(
|
||||
() => {
|
||||
this.setState({
|
||||
domain: '',
|
||||
host: '',
|
||||
name: '',
|
||||
password: '',
|
||||
path: '',
|
||||
port: undefined,
|
||||
type: 'nfs',
|
||||
username: '',
|
||||
})
|
||||
}),
|
||||
injectIntl,
|
||||
provideState({
|
||||
initialState: () => ({
|
||||
formId: generateRandomId(),
|
||||
remote: undefined,
|
||||
}),
|
||||
effects: {
|
||||
reset: () => () => ({
|
||||
formId: generateRandomId(),
|
||||
remote: undefined,
|
||||
}),
|
||||
editRemote: (_, remote) => () => ({
|
||||
remote,
|
||||
}),
|
||||
},
|
||||
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 (
|
||||
}),
|
||||
injectState,
|
||||
({ state, effects, remotes = {}, intl: { formatMessage } }) => (
|
||||
<div>
|
||||
{!isEmpty(remotes.file) && (
|
||||
<div>
|
||||
@ -359,6 +287,7 @@ export default class Remotes extends Component {
|
||||
<SortedTable
|
||||
collection={remotes.file}
|
||||
columns={COLUMNS_LOCAL_REMOTE}
|
||||
data-editRemote={effects.editRemote}
|
||||
data-formatMessage={formatMessage}
|
||||
filters={FILTERS}
|
||||
groupedActions={GROUPED_ACTIONS}
|
||||
@ -374,6 +303,7 @@ export default class Remotes extends Component {
|
||||
<SortedTable
|
||||
collection={remotes.nfs}
|
||||
columns={COLUMNS_NFS_REMOTE}
|
||||
data-editRemote={effects.editRemote}
|
||||
data-formatMessage={formatMessage}
|
||||
filters={FILTERS}
|
||||
groupedActions={GROUPED_ACTIONS}
|
||||
@ -389,6 +319,7 @@ export default class Remotes extends Component {
|
||||
<SortedTable
|
||||
collection={remotes.smb}
|
||||
columns={COLUMNS_SMB_REMOTE}
|
||||
data-editRemote={effects.editRemote}
|
||||
data-formatMessage={formatMessage}
|
||||
filters={FILTERS}
|
||||
groupedActions={GROUPED_ACTIONS}
|
||||
@ -397,162 +328,7 @@ export default class Remotes extends Component {
|
||||
/>
|
||||
</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>
|
||||
)}
|
||||
<Remote formatMessage={formatMessage} key={state.formId} />
|
||||
</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>
|
||||
)
|
||||
}
|
||||
}
|
||||
),
|
||||
].reduceRight((value, decorator) => decorator(value))
|
||||
|
315
packages/xo-web/src/xo-app/settings/remotes/remote.js
Normal file
315
packages/xo-web/src/xo-app/settings/remotes/remote.js
Normal 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))
|
Loading…
Reference in New Issue
Block a user