diff --git a/src/common/intl/messages.js b/src/common/intl/messages.js index 91f62f841..e86f61ad6 100644 --- a/src/common/intl/messages.js +++ b/src/common/intl/messages.js @@ -36,6 +36,8 @@ const messages = { filterSyntaxLinkTooltip: 'Explore the search syntax in the documentation', filterVifsOnlyConnected: 'Connected VIFs', filterVifsOnlyDisconnected: 'Disconnected VIFs', + filterRemotesOnlyConnected: 'Connected remotes', + filterRemotesOnlyDisconnected: 'Disconnected remotes', // ----- Copiable component ----- copyToClipboard: 'Copy to clipboard', @@ -359,16 +361,17 @@ const messages = { remoteConnected: 'Connected', remoteDisconnected: 'Disconnected', remoteDeleteTip: 'Delete', + remoteDeleteSelected: 'Delete selected remotes', remoteNamePlaceHolder: 'remote name *', remoteMyNamePlaceHolder: 'Name *', remoteLocalPlaceHolderPath: '/path/to/backup', remoteNfsPlaceHolderHost: 'host *', remoteNfsPlaceHolderPath: 'path/to/backup', - remoteSmbPlaceHolderRemotePath: 'subfolder [path\\to\\backup]', + remoteSmbPlaceHolderRemotePath: 'subfolder [path\\\\to\\\\backup]', remoteSmbPlaceHolderUsername: 'Username', remoteSmbPlaceHolderPassword: 'Password', remoteSmbPlaceHolderDomain: 'Domain', - remoteSmbPlaceHolderAddressShare: '
\\ *', + remoteSmbPlaceHolderAddressShare: '
\\\\ *', remotePlaceHolderPassword: 'password(fill to edit)', // ------ New Storage ----- @@ -1238,6 +1241,9 @@ const messages = { 'Delete schedule{nSchedules, plural, one {} other {s}}', deleteSchedulesModalMessage: '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', deleteVifsModalTitle: 'Delete VIF{nVifs, plural, one {} other {s}}', deleteVifsModalMessage: diff --git a/src/common/xo/index.js b/src/common/xo/index.js index a59f3fd39..4cd9b37ac 100644 --- a/src/common/xo/index.js +++ b/src/common/xo/index.js @@ -1659,6 +1659,20 @@ export const deleteRemote = remote => 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 => _call('remote.set', { id: resolveId(remote), enabled: true })::tap( subscribeRemotes.forceRefresh diff --git a/src/xo-app/settings/remotes/index.js b/src/xo-app/settings/remotes/index.js index c67982121..f956d3b26 100644 --- a/src/xo-app/settings/remotes/index.js +++ b/src/xo-app/settings/remotes/index.js @@ -1,12 +1,12 @@ import _, { messages } from 'intl' import ActionButton from 'action-button' -import ActionRowButton from 'action-row-button' import filter from 'lodash/filter' import Icon from 'icon' import isEmpty from 'lodash/isEmpty' import map from 'lodash/map' import React, { Component } from 'react' import some from 'lodash/some' +import SortedTable from 'sorted-table' import StateButton from 'state-button' import Tooltip from 'tooltip' import { addSubscriptions } from 'utils' @@ -19,6 +19,7 @@ import { injectIntl } from 'react-intl' import { createRemote, deleteRemote, + deleteRemotes, disableRemote, editRemote, enableRemote, @@ -31,255 +32,225 @@ const remoteTypes = { nfs: 'remoteTypeNfs', smb: 'remoteTypeSmb', } - -class AbstractRemote extends Component { - _changeUrlElement = (value, element) => { - const remote = { ...this.props.remote } - remote[element] = value - const url = format(remote) - 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 = ( - - {' '} - {_(answer.success ? 'remoteTestSuccess' : 'remoteTestFailure', { - name: remote.name, - })} - +const _changeUrlElement = (remote, value, element) => + editRemote(remote, { url: format({ ...remote, [element]: value }) }) +const _showError = remote => alert(_('remoteConnectionFailed'), remote.error) +const COLUMN_NAME = { + component: @injectIntl + class RemoteName extends Component { + render () { + const { item: remote, intl } = this.props + return ( + editRemote(remote, { name })} + placeholder={intl.formatMessage(messages.remoteMyNamePlaceHolder)} + value={remote.name} + /> ) - let body - if (answer.success) { - body = _('remoteTestSuccessMessage') - } else { - body = ( -

-

-
{_('remoteTestError')}
-
{answer.error}
-
{_('remoteTestStep')}
-
{answer.step}
-
-

+ } + }, + name: _('remoteName'), + sortCriteria: 'name', +} +const COLUMN_STATE = { + itemRenderer: remote => ( +
+ {' '} + {remote.error && ( + + _showError(remote)} + style={{ padding: '0px' }} + > + + + + )} +
+ ), + name: _('remoteState'), +} + +const COLUMNS_LOCAL_REMOTE = [ + COLUMN_NAME, + { + component: @injectIntl + class LocalRemotePath extends Component { + render () { + const { item: remote, intl } = this.props + return ( + _changeUrlElement(remote, v, 'path')} + placeholder={intl.formatMessage( + messages.remoteLocalPlaceHolderPath + )} + value={remote.path} + /> ) } - alert(title, body) - }) - } - - render () { - const { remote } = this.props - - return ( - - - - - - {this._renderRemoteInfo(remote)} - {this._renderAuthInfo(remote)} - - {' '} - {remote.error && ( - - - - - - )} - - - {remote.enabled && ( - - - - )}{' '} - - + \\ + _changeUrlElement(remote, v, 'host')} + placeholder={intl.formatMessage( + messages.remoteNfsPlaceHolderHost + )} + value={remote.host} /> - - - - ) - } + : + _changeUrlElement(remote, v, 'path')} + placeholder={intl.formatMessage( + messages.remoteNfsPlaceHolderPath + )} + value={remote.path} + /> + + ) + } + }, + name: _('remoteDevice'), + }, + COLUMN_STATE, +] +const COLUMNS_SMB_REMOTE = [ + COLUMN_NAME, + { + component: @injectIntl + class SmbRemoteInfo extends Component { + render () { + const { item: remote, intl } = this.props + return ( + + \\ + _changeUrlElement(remote, v, 'host')} + /> + \ + + _changeUrlElement(remote, v, 'path')} + placeholder={intl.formatMessage( + messages.remoteSmbPlaceHolderRemotePath + )} + value={remote.path} + /> + + + ) + } + }, + name: _('remoteShare'), + }, + COLUMN_STATE, + { + component: @injectIntl + class SmbRemoteAuthInfo extends Component { + render () { + const { item: remote, intl } = this.props + return ( + + _changeUrlElement(remote, v, 'username')} + /> + : + _changeUrlElement(remote, v, 'password')} + placeholder={intl.formatMessage( + messages.remotePlaceHolderPassword + )} + /> + @ + _changeUrlElement(remote, v, 'domain')} + /> + + ) + } + }, + name: _('remoteAuth'), + }, +] - _renderRemoteInfo () { - throw new Error('NOT IMPLEMENTED') - } +const GROUPED_ACTIONS = [ + { + handler: deleteRemotes, + icon: 'delete', + label: _('remoteDeleteSelected'), + level: 'danger', + }, +] - _renderAuthInfo () { - throw new Error('NOT IMPLEMENTED') - } - - get accessible () { - throw new Error('NOT IMPLEMENTED') - } - - get unaccessible () { - throw new Error('NOT IMPLEMENTED') - } -} - -@injectIntl -class LocalRemote extends AbstractRemote { - _renderRemoteInfo () { - const { remote } = this.props - return ( - this._changeUrlElement(v, 'path')} - placeholder={this.props.intl.formatMessage( - messages.remoteLocalPlaceHolderPath - )} - /> - ) - } - - _renderAuthInfo () { - return '' - } - - get accessible () { - return 'Accessible' - } - - get unaccessible () { - return 'Unaccessible' - } -} - -@injectIntl -class NfsRemote extends AbstractRemote { - _renderRemoteInfo () { - const { remote } = this.props - return ( - - this._changeUrlElement(v, 'host')} - placeholder={this.props.intl.formatMessage( - messages.remoteNfsPlaceHolderHost - )} - /> - : - this._changeUrlElement(v, 'path')} - placeholder={this.props.intl.formatMessage( - messages.remoteNfsPlaceHolderPath - )} - /> - - ) - } - - _renderAuthInfo () { - return '' - } - - get accessible () { - return _('remoteMounted') - } - - get unaccessible () { - return _('remoteUnmounted') - } -} - -@injectIntl -class SmbRemote extends AbstractRemote { - _renderRemoteInfo () { - const { remote } = this.props - return ( - - \\ - this._changeUrlElement(v, 'host')} - /> - \ - - this._changeUrlElement(v, 'path')} - placeholder={this.props.intl.formatMessage( - messages.remoteSmbPlaceHolderRemotePath - )} - /> - - - ) - } - - _renderAuthInfo () { - const { remote } = this.props - return ( - - this._changeUrlElement(v, 'username')} - /> - : - this._changeUrlElement(v, 'password')} - placeholder={this.props.intl.formatMessage( - messages.remotePlaceHolderPassword - )} - /> - @ - this._changeUrlElement(v, 'domain')} - /> - - ) - } - - get accessible () { - return 'Accessible' - } - - get unaccessible () { - return 'Unaccessible' - } +const INDIVIDUAL_ACTIONS = [ + { + disabled: remote => !remote.enabled, + handler: remote => + testRemote(remote).then( + answer => + answer.success + ? alert( + + {' '} + {_('remoteTestSuccess', { name: remote.name })} + , + _('remoteTestSuccessMessage') + ) + : alert( + + {' '} + {_('remoteTestFailure', { name: remote.name })} + , +

+

+
{_('remoteTestError')}
+
{answer.error}
+
{_('remoteTestStep')}
+
{answer.step}
+
+

+ ) + ), + icon: 'diagnosis', + label: _('remoteTestTip'), + level: 'primary', + }, + { + handler: deleteRemote, + icon: 'delete', + label: _('remoteDeleteTip'), + level: 'danger', + }, +] +const FILTERS = { + filterRemotesOnlyConnected: 'enabled?', + filterRemotesOnlyDisconnected: '!enabled?', } @addSubscriptions({ @@ -351,53 +322,48 @@ export default class Remotes extends Component { return (
- - {!isEmpty(remotes.file) && ( - - - - - - - - - {map(remotes.file, (remote, key) => ( - - ))} - - )} - {!isEmpty(remotes.nfs) && ( - - - - - - - - - {map(remotes.nfs, (remote, key) => ( - - ))} - - )} - {!isEmpty(remotes.smb) && ( - - - - - - - - - - {map(remotes.smb, (remote, key) => ( - - ))} - - )} -
{_('remoteTypeLocal')}{_('remoteName')}{_('remotePath')} - {_('remoteState')}{_('remoteAction')}
{_('remoteTypeNfs')}{_('remoteName')}{_('remoteDevice')} - {_('remoteState')}{_('remoteAction')}
{_('remoteTypeSmb')}{_('remoteName')}{_('remoteShare')}{_('remoteAuth')}{_('remoteState')}{_('remoteAction')}
+ {!isEmpty(remotes.file) && ( +
+

{_('remoteTypeLocal')}

+ +
+ )} + + {!isEmpty(remotes.nfs) && ( +
+

{_('remoteTypeNfs')}

+ +
+ )} + + {!isEmpty(remotes.smb) && ( +
+

{_('remoteTypeSmb')}

+ +
+ )} +

{_('newRemote')}

@@ -449,7 +415,7 @@ export default class Remotes extends Component { )} {type === 'nfs' && ( -
+
-
+
/ )} {type === 'smb' && ( -
+
\\