feat(settings/remotes): use SortedTable (#2465)

See #2416
This commit is contained in:
Rajaa.BARHTAOUI 2018-01-31 15:13:25 +01:00 committed by Pierre Donias
parent 4349911076
commit 2b5f7dc8de
3 changed files with 282 additions and 296 deletions

View File

@ -36,6 +36,8 @@ const messages = {
filterSyntaxLinkTooltip: 'Explore the search syntax in the documentation', filterSyntaxLinkTooltip: 'Explore the search syntax in the documentation',
filterVifsOnlyConnected: 'Connected VIFs', filterVifsOnlyConnected: 'Connected VIFs',
filterVifsOnlyDisconnected: 'Disconnected VIFs', filterVifsOnlyDisconnected: 'Disconnected VIFs',
filterRemotesOnlyConnected: 'Connected remotes',
filterRemotesOnlyDisconnected: 'Disconnected remotes',
// ----- Copiable component ----- // ----- Copiable component -----
copyToClipboard: 'Copy to clipboard', copyToClipboard: 'Copy to clipboard',
@ -359,16 +361,17 @@ const messages = {
remoteConnected: 'Connected', remoteConnected: 'Connected',
remoteDisconnected: 'Disconnected', remoteDisconnected: 'Disconnected',
remoteDeleteTip: 'Delete', remoteDeleteTip: 'Delete',
remoteDeleteSelected: 'Delete selected remotes',
remoteNamePlaceHolder: 'remote name *', remoteNamePlaceHolder: 'remote name *',
remoteMyNamePlaceHolder: 'Name *', remoteMyNamePlaceHolder: 'Name *',
remoteLocalPlaceHolderPath: '/path/to/backup', remoteLocalPlaceHolderPath: '/path/to/backup',
remoteNfsPlaceHolderHost: 'host *', remoteNfsPlaceHolderHost: 'host *',
remoteNfsPlaceHolderPath: 'path/to/backup', remoteNfsPlaceHolderPath: 'path/to/backup',
remoteSmbPlaceHolderRemotePath: 'subfolder [path\\to\\backup]', remoteSmbPlaceHolderRemotePath: 'subfolder [path\\\\to\\\\backup]',
remoteSmbPlaceHolderUsername: 'Username', remoteSmbPlaceHolderUsername: 'Username',
remoteSmbPlaceHolderPassword: 'Password', remoteSmbPlaceHolderPassword: 'Password',
remoteSmbPlaceHolderDomain: 'Domain', remoteSmbPlaceHolderDomain: 'Domain',
remoteSmbPlaceHolderAddressShare: '<address>\\<share> *', remoteSmbPlaceHolderAddressShare: '<address>\\\\<share> *',
remotePlaceHolderPassword: 'password(fill to edit)', remotePlaceHolderPassword: 'password(fill to edit)',
// ------ New Storage ----- // ------ New Storage -----
@ -1238,6 +1241,9 @@ const messages = {
'Delete schedule{nSchedules, plural, one {} other {s}}', 'Delete schedule{nSchedules, plural, one {} other {s}}',
deleteSchedulesModalMessage: deleteSchedulesModalMessage:
'Are you sure you want to delete {nSchedules, number} schedule{nSchedules, plural, one {} other {s}}?', 'Are you sure you want to delete {nSchedules, number} schedule{nSchedules, plural, one {} other {s}}?',
deleteRemotesModalTitle: 'Delete remote{nRemotes, plural, one {} other {s}}',
deleteRemotesModalMessage:
'Are you sure you want to delete {nRemotes, number} remote{nRemotes, plural, one {} other {s}}?',
revertVmModalTitle: 'Revert your VM', revertVmModalTitle: 'Revert your VM',
deleteVifsModalTitle: 'Delete VIF{nVifs, plural, one {} other {s}}', deleteVifsModalTitle: 'Delete VIF{nVifs, plural, one {} other {s}}',
deleteVifsModalMessage: deleteVifsModalMessage:

View File

@ -1659,6 +1659,20 @@ export const deleteRemote = remote =>
subscribeRemotes.forceRefresh subscribeRemotes.forceRefresh
) )
export const deleteRemotes = remotes =>
confirm({
title: _('deleteRemotesModalTitle', { nRemotes: remotes.length }),
body: _('deleteRemotesModalMessage', { nRemotes: remotes.length }),
}).then(
() =>
Promise.all(
map(remotes, remote =>
_call('remote.delete', { id: resolveId(remote) })
)
)::tap(subscribeRemotes.forceRefresh),
noop
)
export const enableRemote = remote => export const enableRemote = remote =>
_call('remote.set', { id: resolveId(remote), enabled: true })::tap( _call('remote.set', { id: resolveId(remote), enabled: true })::tap(
subscribeRemotes.forceRefresh subscribeRemotes.forceRefresh

View File

@ -1,12 +1,12 @@
import _, { messages } from 'intl' import _, { messages } from 'intl'
import ActionButton from 'action-button' import ActionButton from 'action-button'
import ActionRowButton from 'action-row-button'
import filter from 'lodash/filter' import filter from 'lodash/filter'
import Icon from 'icon' import Icon from 'icon'
import isEmpty from 'lodash/isEmpty' import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map' import map from 'lodash/map'
import React, { Component } from 'react' import React, { Component } from 'react'
import some from 'lodash/some' import some from 'lodash/some'
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 } from 'utils'
@ -19,6 +19,7 @@ import { injectIntl } from 'react-intl'
import { import {
createRemote, createRemote,
deleteRemote, deleteRemote,
deleteRemotes,
disableRemote, disableRemote,
editRemote, editRemote,
enableRemote, enableRemote,
@ -31,255 +32,225 @@ const remoteTypes = {
nfs: 'remoteTypeNfs', nfs: 'remoteTypeNfs',
smb: 'remoteTypeSmb', smb: 'remoteTypeSmb',
} }
const _changeUrlElement = (remote, value, element) =>
class AbstractRemote extends Component { editRemote(remote, { url: format({ ...remote, [element]: value }) })
_changeUrlElement = (value, element) => { const _showError = remote => alert(_('remoteConnectionFailed'), remote.error)
const remote = { ...this.props.remote } const COLUMN_NAME = {
remote[element] = value component: @injectIntl
const url = format(remote) class RemoteName extends Component {
return editRemote(remote, { url }) render () {
} const { item: remote, intl } = this.props
return (
_showError = () => alert(_('remoteConnectionFailed'), this.props.remote.error) <Text
onChange={name => editRemote(remote, { name })}
_changeName = name => { placeholder={intl.formatMessage(messages.remoteMyNamePlaceHolder)}
const { remote } = this.props value={remote.name}
return editRemote(remote, { name }) />
}
_test = () => {
const { remote } = this.props
testRemote(remote).then(answer => {
const title = (
<span>
<Icon icon={answer.success ? 'success' : 'error'} />{' '}
{_(answer.success ? 'remoteTestSuccess' : 'remoteTestFailure', {
name: remote.name,
})}
</span>
) )
let body }
if (answer.success) { },
body = _('remoteTestSuccessMessage') name: _('remoteName'),
} else { sortCriteria: 'name',
body = ( }
<p> const COLUMN_STATE = {
<dl className='dl-horizontal'> itemRenderer: remote => (
<dt>{_('remoteTestError')}</dt> <div>
<dd>{answer.error}</dd> <StateButton
<dt>{_('remoteTestStep')}</dt> disabledLabel={_('remoteDisconnected')}
<dd>{answer.step}</dd> disabledHandler={enableRemote}
</dl> disabledTooltip={_('remoteConnectTip')}
</p> enabledLabel={_('remoteConnected')}
enabledHandler={disableRemote}
enabledTooltip={_('remoteDisconnectTip')}
handlerParam={remote}
state={remote.enabled}
/>{' '}
{remote.error && (
<Tooltip content={_('remoteConnectionFailed')}>
<a
className='text-danger btn btn-link'
onClick={() => _showError(remote)}
style={{ padding: '0px' }}
>
<Icon icon='alarm' size='lg' />
</a>
</Tooltip>
)}
</div>
),
name: _('remoteState'),
}
const COLUMNS_LOCAL_REMOTE = [
COLUMN_NAME,
{
component: @injectIntl
class LocalRemotePath extends Component {
render () {
const { item: remote, intl } = this.props
return (
<Text
onChange={v => _changeUrlElement(remote, v, 'path')}
placeholder={intl.formatMessage(
messages.remoteLocalPlaceHolderPath
)}
value={remote.path}
/>
) )
} }
alert(title, body) },
}) name: _('remotePath'),
} },
COLUMN_STATE,
render () { ]
const { remote } = this.props const COLUMNS_NFS_REMOTE = [
COLUMN_NAME,
return ( {
<tr> component: @injectIntl
<td /> class NfsRemoteInfo extends Component {
<td> render () {
<Text const { item: remote, intl } = this.props
value={remote.name} return (
onChange={this._changeName} <span>
placeholder={this.props.intl.formatMessage( <strong className='text-info'>\\</strong>
messages.remoteNamePlaceHolder <Text
)} onChange={v => _changeUrlElement(remote, v, 'host')}
/> placeholder={intl.formatMessage(
</td> messages.remoteNfsPlaceHolderHost
<td>{this._renderRemoteInfo(remote)}</td> )}
<td>{this._renderAuthInfo(remote)}</td> value={remote.host}
<td>
<StateButton
disabledLabel={_('remoteDisconnected')}
disabledHandler={enableRemote}
disabledTooltip={_('remoteConnectTip')}
enabledLabel={_('remoteConnected')}
enabledHandler={disableRemote}
enabledTooltip={_('remoteDisconnectTip')}
handlerParam={remote}
state={remote.enabled}
/>{' '}
{remote.error && (
<Tooltip content={_('remoteConnectionFailed')}>
<a
className='text-danger btn btn-link'
style={{ padding: '0px' }}
onClick={this._showError}
>
<Icon icon='alarm' size='lg' />
</a>
</Tooltip>
)}
</td>
<td className='text-xs-right'>
{remote.enabled && (
<Tooltip content={_('remoteTestTip')}>
<ActionRowButton
btnStyle='primary'
handler={this._test}
icon='diagnosis'
/>
</Tooltip>
)}{' '}
<Tooltip content={_('remoteDeleteTip')}>
<ActionRowButton
btnStyle='danger'
handler={deleteRemote}
handlerParam={remote}
icon='delete'
/> />
</Tooltip> :
</td> <Text
</tr> onChange={v => _changeUrlElement(remote, v, 'path')}
) placeholder={intl.formatMessage(
} messages.remoteNfsPlaceHolderPath
)}
value={remote.path}
/>
</span>
)
}
},
name: _('remoteDevice'),
},
COLUMN_STATE,
]
const COLUMNS_SMB_REMOTE = [
COLUMN_NAME,
{
component: @injectIntl
class SmbRemoteInfo extends Component {
render () {
const { item: remote, intl } = this.props
return (
<span>
<strong className='text-info'>\\</strong>
<Text
value={remote.host}
onChange={v => _changeUrlElement(remote, v, 'host')}
/>
<strong className='text-info'>\</strong>
<span>
<Text
onChange={v => _changeUrlElement(remote, v, 'path')}
placeholder={intl.formatMessage(
messages.remoteSmbPlaceHolderRemotePath
)}
value={remote.path}
/>
</span>
</span>
)
}
},
name: _('remoteShare'),
},
COLUMN_STATE,
{
component: @injectIntl
class SmbRemoteAuthInfo extends Component {
render () {
const { item: remote, intl } = this.props
return (
<span>
<Text
value={remote.username}
onChange={v => _changeUrlElement(remote, v, 'username')}
/>
:
<Password
value=''
onChange={v => _changeUrlElement(remote, v, 'password')}
placeholder={intl.formatMessage(
messages.remotePlaceHolderPassword
)}
/>
@
<Text
value={remote.domain}
onChange={v => _changeUrlElement(remote, v, 'domain')}
/>
</span>
)
}
},
name: _('remoteAuth'),
},
]
_renderRemoteInfo () { const GROUPED_ACTIONS = [
throw new Error('NOT IMPLEMENTED') {
} handler: deleteRemotes,
icon: 'delete',
label: _('remoteDeleteSelected'),
level: 'danger',
},
]
_renderAuthInfo () { const INDIVIDUAL_ACTIONS = [
throw new Error('NOT IMPLEMENTED') {
} disabled: remote => !remote.enabled,
handler: remote =>
get accessible () { testRemote(remote).then(
throw new Error('NOT IMPLEMENTED') answer =>
} answer.success
? alert(
get unaccessible () { <span>
throw new Error('NOT IMPLEMENTED') <Icon icon='success' />{' '}
} {_('remoteTestSuccess', { name: remote.name })}
} </span>,
_('remoteTestSuccessMessage')
@injectIntl )
class LocalRemote extends AbstractRemote { : alert(
_renderRemoteInfo () { <span>
const { remote } = this.props <Icon icon='error' />{' '}
return ( {_('remoteTestFailure', { name: remote.name })}
<Text </span>,
value={remote.path} <p>
onChange={v => this._changeUrlElement(v, 'path')} <dl className='dl-horizontal'>
placeholder={this.props.intl.formatMessage( <dt>{_('remoteTestError')}</dt>
messages.remoteLocalPlaceHolderPath <dd>{answer.error}</dd>
)} <dt>{_('remoteTestStep')}</dt>
/> <dd>{answer.step}</dd>
) </dl>
} </p>
)
_renderAuthInfo () { ),
return '' icon: 'diagnosis',
} label: _('remoteTestTip'),
level: 'primary',
get accessible () { },
return 'Accessible' {
} handler: deleteRemote,
icon: 'delete',
get unaccessible () { label: _('remoteDeleteTip'),
return 'Unaccessible' level: 'danger',
} },
} ]
const FILTERS = {
@injectIntl filterRemotesOnlyConnected: 'enabled?',
class NfsRemote extends AbstractRemote { filterRemotesOnlyDisconnected: '!enabled?',
_renderRemoteInfo () {
const { remote } = this.props
return (
<span>
<Text
value={remote.host}
onChange={v => this._changeUrlElement(v, 'host')}
placeholder={this.props.intl.formatMessage(
messages.remoteNfsPlaceHolderHost
)}
/>
:
<Text
value={remote.path}
onChange={v => this._changeUrlElement(v, 'path')}
placeholder={this.props.intl.formatMessage(
messages.remoteNfsPlaceHolderPath
)}
/>
</span>
)
}
_renderAuthInfo () {
return ''
}
get accessible () {
return _('remoteMounted')
}
get unaccessible () {
return _('remoteUnmounted')
}
}
@injectIntl
class SmbRemote extends AbstractRemote {
_renderRemoteInfo () {
const { remote } = this.props
return (
<span>
<strong className='text-info'>\\</strong>
<Text
value={remote.host}
onChange={v => this._changeUrlElement(v, 'host')}
/>
<strong className='text-info'>\</strong>
<span>
<Text
value={remote.path}
onChange={v => this._changeUrlElement(v, 'path')}
placeholder={this.props.intl.formatMessage(
messages.remoteSmbPlaceHolderRemotePath
)}
/>
</span>
</span>
)
}
_renderAuthInfo () {
const { remote } = this.props
return (
<span>
<Text
value={remote.username}
onChange={v => this._changeUrlElement(v, 'username')}
/>
:
<Password
value=''
onChange={v => this._changeUrlElement(v, 'password')}
placeholder={this.props.intl.formatMessage(
messages.remotePlaceHolderPassword
)}
/>
@
<Text
value={remote.domain}
onChange={v => this._changeUrlElement(v, 'domain')}
/>
</span>
)
}
get accessible () {
return 'Accessible'
}
get unaccessible () {
return 'Unaccessible'
}
} }
@addSubscriptions({ @addSubscriptions({
@ -351,53 +322,48 @@ export default class Remotes extends Component {
return ( return (
<div> <div>
<table className='table table-hover'> {!isEmpty(remotes.file) && (
{!isEmpty(remotes.file) && ( <div>
<tbody> <h2>{_('remoteTypeLocal')}</h2>
<tr> <SortedTable
<th className='text-info'>{_('remoteTypeLocal')}</th> collection={remotes.file}
<th>{_('remoteName')}</th> columns={COLUMNS_LOCAL_REMOTE}
<th>{_('remotePath')}</th> filters={FILTERS}
<th /> groupedActions={GROUPED_ACTIONS}
<th>{_('remoteState')}</th> individualActions={INDIVIDUAL_ACTIONS}
<th className='text-xs-right'>{_('remoteAction')}</th> stateUrlParam='l'
</tr> />
{map(remotes.file, (remote, key) => ( </div>
<LocalRemote remote={remote} key={key} /> )}
))}
</tbody> {!isEmpty(remotes.nfs) && (
)} <div>
{!isEmpty(remotes.nfs) && ( <h2>{_('remoteTypeNfs')}</h2>
<tbody> <SortedTable
<tr> collection={remotes.nfs}
<th className='text-info'>{_('remoteTypeNfs')}</th> columns={COLUMNS_NFS_REMOTE}
<th>{_('remoteName')}</th> filters={FILTERS}
<th>{_('remoteDevice')}</th> groupedActions={GROUPED_ACTIONS}
<th /> individualActions={INDIVIDUAL_ACTIONS}
<th>{_('remoteState')}</th> stateUrlParam='nfs'
<th className='text-xs-right'>{_('remoteAction')}</th> />
</tr> </div>
{map(remotes.nfs, (remote, key) => ( )}
<NfsRemote remote={remote} key={key} />
))} {!isEmpty(remotes.smb) && (
</tbody> <div>
)} <h2>{_('remoteTypeSmb')}</h2>
{!isEmpty(remotes.smb) && ( <SortedTable
<tbody> collection={remotes.smb}
<tr> columns={COLUMNS_SMB_REMOTE}
<th className='text-info'>{_('remoteTypeSmb')}</th> filters={FILTERS}
<th>{_('remoteName')}</th> groupedActions={GROUPED_ACTIONS}
<th>{_('remoteShare')}</th> individualActions={INDIVIDUAL_ACTIONS}
<th>{_('remoteAuth')}</th> stateUrlParam='smb'
<th>{_('remoteState')}</th> />
<th className='text-xs-right'>{_('remoteAction')}</th> </div>
</tr> )}
{map(remotes.smb, (remote, key) => (
<SmbRemote remote={remote} key={key} />
))}
</tbody>
)}
</table>
<h2>{_('newRemote')}</h2> <h2>{_('newRemote')}</h2>
<form id='newRemoteForm'> <form id='newRemoteForm'>
<div className='form-group'> <div className='form-group'>
@ -449,7 +415,7 @@ export default class Remotes extends Component {
</fieldset> </fieldset>
)} )}
{type === 'nfs' && ( {type === 'nfs' && (
<fieldset className='form-group'> <fieldset>
<div className='form-group'> <div className='form-group'>
<input <input
type='text' type='text'
@ -461,7 +427,7 @@ export default class Remotes extends Component {
required required
/> />
</div> </div>
<div className='input-group'> <div className='input-group form-group'>
<span className='input-group-addon'>/</span> <span className='input-group-addon'>/</span>
<input <input
type='text' type='text'
@ -476,7 +442,7 @@ export default class Remotes extends Component {
</fieldset> </fieldset>
)} )}
{type === 'smb' && ( {type === 'smb' && (
<fieldset className='form-group'> <fieldset>
<div className='input-group form-group'> <div className='input-group form-group'>
<span className='input-group-addon'>\\</span> <span className='input-group-addon'>\\</span>
<input <input