diff --git a/CHANGELOG.md b/CHANGELOG.md index 42409ede4..50591a550 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/packages/xo-web/src/common/freactal-utils.js b/packages/xo-web/src/common/freactal-utils.js new file mode 100644 index 000000000..9ff1c060f --- /dev/null +++ b/packages/xo-web/src/common/freactal-utils.js @@ -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], +}) diff --git a/packages/xo-web/src/xo-app/settings/remotes/index.js b/packages/xo-web/src/xo-app/settings/remotes/index.js index fe9f7f658..c980b50f8 100644 --- a/packages/xo-web/src/xo-app/settings/remotes/index.js +++ b/packages/xo-web/src/xo-app/settings/remotes/index.js @@ -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,317 +241,94 @@ const FILTERS = { filterRemotesOnlyDisconnected: '!enabled?', } -@addSubscriptions({ - remotes: cb => - subscribeRemotes(remotes => { - cb( - groupBy( - map(remotes, remote => { - try { - return { - ...remote, - ...parse(remote.url), +export default [ + addSubscriptions({ + remotes: cb => + subscribeRemotes(remotes => { + cb( + groupBy( + map(remotes, remote => { + try { + return { + ...remote, + ...parse(remote.url), + } + } catch (err) { + console.error('Remote parsing error:', remote, '\n', err) } - } catch (err) { - console.error('Remote parsing error:', remote, '\n', err) - } - }).filter(r => r !== undefined), - 'type' + }).filter(r => r !== undefined), + 'type' + ) ) - ) + }), + }), + injectIntl, + provideState({ + initialState: () => ({ + formId: generateRandomId(), + remote: undefined, }), -}) -@injectIntl -export default class Remotes extends Component { - constructor (props) { - super(props) - this.state = { - domain: '', - host: '', - name: '', - password: '', - path: '', - port: undefined, - type: 'nfs', - username: '', - } - } + effects: { + reset: () => () => ({ + formId: generateRandomId(), + remote: undefined, + }), + editRemote: (_, remote) => () => ({ + remote, + }), + }, + }), + injectState, + ({ state, effects, remotes = {}, intl: { formatMessage } }) => ( +
{_('remoteTestNameFailure')}
- ) - : this._createRemote() + {!isEmpty(remotes.nfs) && ( +{_('remoteTestNameFailure')}
+ ) + } + + 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 ( +