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,70 +32,29 @@ 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 })
}
_showError = () => alert(_('remoteConnectionFailed'), this.props.remote.error)
_changeName = name => {
const { remote } = this.props
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')
} else {
body = (
<p>
<dl className='dl-horizontal'>
<dt>{_('remoteTestError')}</dt>
<dd>{answer.error}</dd>
<dt>{_('remoteTestStep')}</dt>
<dd>{answer.step}</dd>
</dl>
</p>
)
}
alert(title, body)
})
}
render () { render () {
const { remote } = this.props const { item: remote, intl } = this.props
return ( return (
<tr>
<td />
<td>
<Text <Text
onChange={name => editRemote(remote, { name })}
placeholder={intl.formatMessage(messages.remoteMyNamePlaceHolder)}
value={remote.name} value={remote.name}
onChange={this._changeName}
placeholder={this.props.intl.formatMessage(
messages.remoteNamePlaceHolder
)}
/> />
</td> )
<td>{this._renderRemoteInfo(remote)}</td> }
<td>{this._renderAuthInfo(remote)}</td> },
<td> name: _('remoteName'),
sortCriteria: 'name',
}
const COLUMN_STATE = {
itemRenderer: remote => (
<div>
<StateButton <StateButton
disabledLabel={_('remoteDisconnected')} disabledLabel={_('remoteDisconnected')}
disabledHandler={enableRemote} disabledHandler={enableRemote}
@ -109,177 +69,188 @@ class AbstractRemote extends Component {
<Tooltip content={_('remoteConnectionFailed')}> <Tooltip content={_('remoteConnectionFailed')}>
<a <a
className='text-danger btn btn-link' className='text-danger btn btn-link'
onClick={() => _showError(remote)}
style={{ padding: '0px' }} style={{ padding: '0px' }}
onClick={this._showError}
> >
<Icon icon='alarm' size='lg' /> <Icon icon='alarm' size='lg' />
</a> </a>
</Tooltip> </Tooltip>
)} )}
</td> </div>
<td className='text-xs-right'> ),
{remote.enabled && ( name: _('remoteState'),
<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>
</tr>
)
}
_renderRemoteInfo () {
throw new Error('NOT IMPLEMENTED')
}
_renderAuthInfo () {
throw new Error('NOT IMPLEMENTED')
}
get accessible () {
throw new Error('NOT IMPLEMENTED')
}
get unaccessible () {
throw new Error('NOT IMPLEMENTED')
}
} }
@injectIntl const COLUMNS_LOCAL_REMOTE = [
class LocalRemote extends AbstractRemote { COLUMN_NAME,
_renderRemoteInfo () { {
const { remote } = this.props component: @injectIntl
class LocalRemotePath extends Component {
render () {
const { item: remote, intl } = this.props
return ( return (
<Text <Text
value={remote.path} onChange={v => _changeUrlElement(remote, v, 'path')}
onChange={v => this._changeUrlElement(v, 'path')} placeholder={intl.formatMessage(
placeholder={this.props.intl.formatMessage(
messages.remoteLocalPlaceHolderPath messages.remoteLocalPlaceHolderPath
)} )}
value={remote.path}
/> />
) )
} }
},
_renderAuthInfo () { name: _('remotePath'),
return '' },
} COLUMN_STATE,
]
get accessible () { const COLUMNS_NFS_REMOTE = [
return 'Accessible' COLUMN_NAME,
} {
component: @injectIntl
get unaccessible () { class NfsRemoteInfo extends Component {
return 'Unaccessible' render () {
} const { item: remote, intl } = this.props
}
@injectIntl
class NfsRemote extends AbstractRemote {
_renderRemoteInfo () {
const { remote } = this.props
return ( return (
<span> <span>
<strong className='text-info'>\\</strong>
<Text <Text
value={remote.host} onChange={v => _changeUrlElement(remote, v, 'host')}
onChange={v => this._changeUrlElement(v, 'host')} placeholder={intl.formatMessage(
placeholder={this.props.intl.formatMessage(
messages.remoteNfsPlaceHolderHost messages.remoteNfsPlaceHolderHost
)} )}
value={remote.host}
/> />
: :
<Text <Text
value={remote.path} onChange={v => _changeUrlElement(remote, v, 'path')}
onChange={v => this._changeUrlElement(v, 'path')} placeholder={intl.formatMessage(
placeholder={this.props.intl.formatMessage(
messages.remoteNfsPlaceHolderPath messages.remoteNfsPlaceHolderPath
)} )}
value={remote.path}
/> />
</span> </span>
) )
} }
},
_renderAuthInfo () { name: _('remoteDevice'),
return '' },
} COLUMN_STATE,
]
get accessible () { const COLUMNS_SMB_REMOTE = [
return _('remoteMounted') COLUMN_NAME,
} {
component: @injectIntl
get unaccessible () { class SmbRemoteInfo extends Component {
return _('remoteUnmounted') render () {
} const { item: remote, intl } = this.props
}
@injectIntl
class SmbRemote extends AbstractRemote {
_renderRemoteInfo () {
const { remote } = this.props
return ( return (
<span> <span>
<strong className='text-info'>\\</strong> <strong className='text-info'>\\</strong>
<Text <Text
value={remote.host} value={remote.host}
onChange={v => this._changeUrlElement(v, 'host')} onChange={v => _changeUrlElement(remote, v, 'host')}
/> />
<strong className='text-info'>\</strong> <strong className='text-info'>\</strong>
<span> <span>
<Text <Text
value={remote.path} onChange={v => _changeUrlElement(remote, v, 'path')}
onChange={v => this._changeUrlElement(v, 'path')} placeholder={intl.formatMessage(
placeholder={this.props.intl.formatMessage(
messages.remoteSmbPlaceHolderRemotePath messages.remoteSmbPlaceHolderRemotePath
)} )}
value={remote.path}
/> />
</span> </span>
</span> </span>
) )
} }
},
_renderAuthInfo () { name: _('remoteShare'),
const { remote } = this.props },
COLUMN_STATE,
{
component: @injectIntl
class SmbRemoteAuthInfo extends Component {
render () {
const { item: remote, intl } = this.props
return ( return (
<span> <span>
<Text <Text
value={remote.username} value={remote.username}
onChange={v => this._changeUrlElement(v, 'username')} onChange={v => _changeUrlElement(remote, v, 'username')}
/> />
: :
<Password <Password
value='' value=''
onChange={v => this._changeUrlElement(v, 'password')} onChange={v => _changeUrlElement(remote, v, 'password')}
placeholder={this.props.intl.formatMessage( placeholder={intl.formatMessage(
messages.remotePlaceHolderPassword messages.remotePlaceHolderPassword
)} )}
/> />
@ @
<Text <Text
value={remote.domain} value={remote.domain}
onChange={v => this._changeUrlElement(v, 'domain')} onChange={v => _changeUrlElement(remote, v, 'domain')}
/> />
</span> </span>
) )
} }
},
name: _('remoteAuth'),
},
]
get accessible () { const GROUPED_ACTIONS = [
return 'Accessible' {
} handler: deleteRemotes,
icon: 'delete',
label: _('remoteDeleteSelected'),
level: 'danger',
},
]
get unaccessible () { const INDIVIDUAL_ACTIONS = [
return 'Unaccessible' {
} disabled: remote => !remote.enabled,
handler: remote =>
testRemote(remote).then(
answer =>
answer.success
? alert(
<span>
<Icon icon='success' />{' '}
{_('remoteTestSuccess', { name: remote.name })}
</span>,
_('remoteTestSuccessMessage')
)
: alert(
<span>
<Icon icon='error' />{' '}
{_('remoteTestFailure', { name: remote.name })}
</span>,
<p>
<dl className='dl-horizontal'>
<dt>{_('remoteTestError')}</dt>
<dd>{answer.error}</dd>
<dt>{_('remoteTestStep')}</dt>
<dd>{answer.step}</dd>
</dl>
</p>
)
),
icon: 'diagnosis',
label: _('remoteTestTip'),
level: 'primary',
},
{
handler: deleteRemote,
icon: 'delete',
label: _('remoteDeleteTip'),
level: 'danger',
},
]
const FILTERS = {
filterRemotesOnlyConnected: 'enabled?',
filterRemotesOnlyDisconnected: '!enabled?',
} }
@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) && (
<tbody> <div>
<tr> <h2>{_('remoteTypeLocal')}</h2>
<th className='text-info'>{_('remoteTypeLocal')}</th> <SortedTable
<th>{_('remoteName')}</th> collection={remotes.file}
<th>{_('remotePath')}</th> columns={COLUMNS_LOCAL_REMOTE}
<th /> filters={FILTERS}
<th>{_('remoteState')}</th> groupedActions={GROUPED_ACTIONS}
<th className='text-xs-right'>{_('remoteAction')}</th> individualActions={INDIVIDUAL_ACTIONS}
</tr> stateUrlParam='l'
{map(remotes.file, (remote, key) => ( />
<LocalRemote remote={remote} key={key} /> </div>
))}
</tbody>
)} )}
{!isEmpty(remotes.nfs) && ( {!isEmpty(remotes.nfs) && (
<tbody> <div>
<tr> <h2>{_('remoteTypeNfs')}</h2>
<th className='text-info'>{_('remoteTypeNfs')}</th> <SortedTable
<th>{_('remoteName')}</th> collection={remotes.nfs}
<th>{_('remoteDevice')}</th> columns={COLUMNS_NFS_REMOTE}
<th /> filters={FILTERS}
<th>{_('remoteState')}</th> groupedActions={GROUPED_ACTIONS}
<th className='text-xs-right'>{_('remoteAction')}</th> individualActions={INDIVIDUAL_ACTIONS}
</tr> stateUrlParam='nfs'
{map(remotes.nfs, (remote, key) => ( />
<NfsRemote remote={remote} key={key} /> </div>
))}
</tbody>
)} )}
{!isEmpty(remotes.smb) && ( {!isEmpty(remotes.smb) && (
<tbody> <div>
<tr> <h2>{_('remoteTypeSmb')}</h2>
<th className='text-info'>{_('remoteTypeSmb')}</th> <SortedTable
<th>{_('remoteName')}</th> collection={remotes.smb}
<th>{_('remoteShare')}</th> columns={COLUMNS_SMB_REMOTE}
<th>{_('remoteAuth')}</th> filters={FILTERS}
<th>{_('remoteState')}</th> groupedActions={GROUPED_ACTIONS}
<th className='text-xs-right'>{_('remoteAction')}</th> individualActions={INDIVIDUAL_ACTIONS}
</tr> stateUrlParam='smb'
{map(remotes.smb, (remote, key) => ( />
<SmbRemote remote={remote} key={key} /> </div>
))}
</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