diff --git a/src/common/messages.js b/src/common/messages.js index 6c03c721f..bfcb3027f 100644 --- a/src/common/messages.js +++ b/src/common/messages.js @@ -358,6 +358,25 @@ export const messages = { defaultMessage: 'Select your backup type:' }, + // ------ New Remote ----- + remoteList: { + defaultMessage: 'Remote stores for backup' + }, + newRemote: { + defaultMessage: 'New File System Remote' + }, + remoteTypeLocal: { + defaultMessage: 'Local' + }, + remoteTypeNfs: { + defaultMessage: 'NFS' + }, + remoteTypeSmb: { + defaultMessage: 'SMB' + }, + remoteType: { + defaultMessage: 'Type' + }, // ----- Plugins ------ autoloadPlugin: { diff --git a/src/common/xo.js b/src/common/xo.js index 7904003ff..3850b3618 100644 --- a/src/common/xo.js +++ b/src/common/xo.js @@ -492,3 +492,21 @@ export const purgePluginConfiguration = async id => { throw error } } + +// ------------------------------------------------------------------- + +export const createRemote = (name, url) => ( + xo.call('remote.create', {name, url}) +) + +export const deleteRemote = id => ( + xo.call('remote.delete', {id}) +) + +export const enableRemote = id => ( + xo.call('remote.set', {id, enabled: true}) +) + +export const disableRemote = id => ( + xo.call('remote.set', {id, enabled: false}) +) diff --git a/src/icons.scss b/src/icons.scss index 50cc1e13d..975f48c02 100644 --- a/src/icons.scss +++ b/src/icons.scss @@ -111,6 +111,10 @@ @extend .fa; @extend .fa-database; } + &-delete { + @extend .fa; + @extend .fa-trash; + } // Backups &-backup { diff --git a/src/xo-app/index.js b/src/xo-app/index.js index ba9aaee64..2a329098d 100644 --- a/src/xo-app/index.js +++ b/src/xo-app/index.js @@ -28,8 +28,8 @@ import Vm from './vm' dashboard: Dashboard, home: Home, 'hosts/:id': Host, - 'pools/:id': Pool, new: New, + 'pools/:id': Pool, settings: Settings, 'srs/:id': Sr, user: User, diff --git a/src/xo-app/settings/index.js b/src/xo-app/settings/index.js index 028e86d23..11e7485c6 100644 --- a/src/xo-app/settings/index.js +++ b/src/xo-app/settings/index.js @@ -1,10 +1,12 @@ import { routes } from 'utils' import Plugins from './plugins' +import Remotes from './remotes' import Servers from './servers' const Settings = routes('servers', { plugins: Plugins, + remotes: Remotes, servers: Servers })( ({ children }) => children diff --git a/src/xo-app/settings/remotes/index.js b/src/xo-app/settings/remotes/index.js new file mode 100644 index 000000000..f58232c82 --- /dev/null +++ b/src/xo-app/settings/remotes/index.js @@ -0,0 +1,303 @@ +import _ from 'messages' +import ActionButton from 'action-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 { error } from 'notification' +import { + createRemote, + deleteRemote, + disableRemote, + enableRemote, + subscribeRemotes +} from 'xo' +import { format, parse } from 'xo-remote-parser' + +const remoteTypes = { + local: 'remoteTypeLocal', + nfs: 'remoteTypeNfs', + smb: 'remoteTypeSmb' +} + +class AbstractRemote extends Component { + _disable = id => disableRemote(id).then(subscribeRemotes.forceRefresh) + _enable = id => enableRemote(id).then(subscribeRemotes.forceRefresh) + _delete = id => deleteRemote(id).then(subscribeRemotes.forceRefresh) + render () { + const { + remote + } = this.props + + return + + {remote.name} + {this._renderRemoteInfo(remote)} + {this._renderAuthInfo(remote)} + + {remote.enabled && + + {this.accessible} + {' '} + + + } + {!remote.enabled && + + {this.unaccessible} + {' '} + + + } + + {remote.error} + + + } + + _renderRemoteInfo (remote) { + throw new Error('NOT IMPLEMENTED') + } + + _renderAuthInfo (remote) { + throw new Error('NOT IMPLEMENTED') + } + + get accessible () { + throw new Error('NOT IMPLEMENTED') + } + + get unaccessible () { + throw new Error('NOT IMPLEMENTED') + } +} + +class LocalRemote extends AbstractRemote { + _renderRemoteInfo (remote) { + return remote.path + } + + _renderAuthInfo (remote) { + return '' + } + + get accessible () { + return 'Accessible' + } + + get unaccessible () { + return 'Unaccessible' + } +} + +class NfsRemote extends AbstractRemote { + _renderRemoteInfo (remote) { + return `${remote.host}:${remote.share}` + } + + _renderAuthInfo (remote) { + return '' + } + + get accessible () { + return 'Mounted' + } + + get unaccessible () { + return 'Unmounted' + } +} + +class SmbRemote extends AbstractRemote { + _renderRemoteInfo (remote) { + return \\{remote.host}{remote.path && \{remote.path}} + } + + _renderAuthInfo (remote) { + return {remote.username}@{remote.domain} + } + + get accessible () { + return 'Accessible' + } + + get unaccessible () { + return 'Unaccessible' + } +} + +export default class New extends Component { + constructor (props) { + super(props) + this.state = { + remotes: {}, + type: 'local' + } + } + + componentWillMount () { + this.componentWillUnmount = subscribeRemotes(rawRemotes => { + rawRemotes = map(rawRemotes, parse) + const remotes = {} + for (let type in remoteTypes) { + remotes[type] = filter(rawRemotes, r => r.type === type) + } + this.setState({ + remotes + }) + }) + } + + _handleRemoteTypeSelection = type => this.setState({type}) + + _createRemote = async () => { + const { + name, + host, + path, + username, + password, + domain + } = this.refs + const { + type + } = this.state + + const urlParams = { + type, + host: host && host.value, + path: path && path.value + } + username && (urlParams.username = username.value) + password && (urlParams.password = password.value) + domain && (urlParams.domain = domain.value) + + const url = format(urlParams) + return createRemote(name && name.value, url).then(() => { + subscribeRemotes.forceRefresh() + this.setState({type: 'local'}) + path && (path.value = '') + username && (username.value = '') + password && (password.value = '') + domain && (domain.value = '') + }, err => error('Create Remote', err.message || String(err))) + } + + render () { + const { + remotes, + type + } = this.state + return ( +
+

{_('remoteList')}

+ + {!isEmpty(remotes.local) && + + + + + + + + + + + {map(remotes.local, (remote, key) => )} + + } + {!isEmpty(remotes.nfs) && + + + + + + + + + + + {map(remotes.nfs, (remote, key) => )} + + } + {!isEmpty(remotes.smb) && + + + + + + + + + + + {map(remotes.smb, (remote, key) => )} + + } +
LocalNamePathStateError
NFSNameDeviceStateError
SMBNameShareAuthStateError
+

{_('newRemote')}

+
+
+ + +
+
+ +
+ {type === 'local' && +
+
+ / + +
+
+ } + {type === 'nfs' && +
+
+ +
+
+ / + +
+
+ } + {type === 'smb' && +
+
+ \\ + + \ + +
+
+ +
+
+ +
+
+ +
+
+ } +
+ + {_('savePluginConfiguration')} + +
+
+
+ ) + } +}