diff --git a/packages/xo-web/package.json b/packages/xo-web/package.json index ed69069ec..7a08bcdc3 100644 --- a/packages/xo-web/package.json +++ b/packages/xo-web/package.json @@ -97,7 +97,7 @@ "promise-toolbox": "^0.10.1", "prop-types": "^15.6.0", "random-password": "^0.1.2", - "reaclette": "^0.6.0", + "reaclette": "^0.7.0", "react": "^15.4.1", "react-addons-shallow-compare": "^15.6.2", "react-addons-test-utils": "^15.6.2", diff --git a/packages/xo-web/src/common/store/actions.js b/packages/xo-web/src/common/store/actions.js index 9747249d4..773915814 100644 --- a/packages/xo-web/src/common/store/actions.js +++ b/packages/xo-web/src/common/store/actions.js @@ -39,14 +39,17 @@ export const updatePermissions = createAction( export const signedIn = createAction('SIGNED_IN', user => user) export const signedOut = createAction('SIGNED_OUT') -export const xoaUpdaterState = createAction('XOA_UPDATER_STATE', state => state) -export const xoaTrialState = createAction('XOA_TRIAL_STATE', state => state) -export const xoaUpdaterLog = createAction('XOA_UPDATER_LOG', log => log) -export const xoaRegisterState = createAction( +export const setXoaUpdaterState = createAction( + 'XOA_UPDATER_STATE', + state => state +) +export const setXoaTrialState = createAction('XOA_TRIAL_STATE', state => state) +export const setXoaUpdaterLog = createAction('XOA_UPDATER_LOG', log => log) +export const setXoaRegisterState = createAction( 'XOA_REGISTER_STATE', registration => registration ) -export const xoaConfiguration = createAction( +export const setXoaConfiguration = createAction( 'XOA_CONFIGURATION', configuration => configuration ) diff --git a/packages/xo-web/src/common/store/reducer.js b/packages/xo-web/src/common/store/reducer.js index 0cc75dd19..ac13abbb2 100644 --- a/packages/xo-web/src/common/store/reducer.js +++ b/packages/xo-web/src/common/store/reducer.js @@ -136,28 +136,28 @@ export default { }), xoaUpdaterState: combineActionHandlers('disconnected', { - [actions.xoaUpdaterState]: (_, state) => state, + [actions.setXoaUpdaterState]: (_, state) => state, }), xoaTrialState: combineActionHandlers( {}, { - [actions.xoaTrialState]: (_, state) => state, + [actions.setXoaTrialState]: (_, state) => state, } ), xoaUpdaterLog: combineActionHandlers([], { - [actions.xoaUpdaterLog]: (_, log) => log, + [actions.setXoaUpdaterLog]: (_, log) => log, }), xoaRegisterState: combineActionHandlers( { state: '?' }, { - [actions.xoaRegisterState]: (_, registration) => registration, + [actions.setXoaRegisterState]: (_, registration) => registration, } ), xoaConfiguration: combineActionHandlers( { proxyHost: '', proxyPort: '', proxyUser: '' }, { // defined values for controlled inputs - [actions.xoaConfiguration]: (_, configuration) => { + [actions.setXoaConfiguration]: (_, configuration) => { delete configuration.password return configuration }, diff --git a/packages/xo-web/src/common/utils.js b/packages/xo-web/src/common/utils.js index 3bf95b427..a340761d9 100644 --- a/packages/xo-web/src/common/utils.js +++ b/packages/xo-web/src/common/utils.js @@ -18,6 +18,7 @@ import { keys, map, mapValues, + pick, replace, sample, some, @@ -69,6 +70,11 @@ export const propsEqual = (o1, o2, props) => { // =================================================================== const _normalizeMapStateToProps = mapper => { + // accept a list of entries to extract from the state + if (Array.isArray(mapper)) { + return state => pick(state, mapper) + } + if (isFunction(mapper)) { const factoryOrMapper = (state, props) => { const result = mapper(state, props) diff --git a/packages/xo-web/src/common/xoa-updater.js b/packages/xo-web/src/common/xoa-updater.js index 1c80d5546..ad8832021 100644 --- a/packages/xo-web/src/common/xoa-updater.js +++ b/packages/xo-web/src/common/xoa-updater.js @@ -9,11 +9,11 @@ import makeError from 'make-error' import map from 'lodash/map' import { EventEmitter } from 'events' import { - xoaConfiguration, - xoaRegisterState, - xoaTrialState, - xoaUpdaterLog, - xoaUpdaterState, + setXoaConfiguration, + setXoaRegisterState, + setXoaTrialState, + setXoaUpdaterLog, + setXoaUpdaterState, } from 'store/actions' // =================================================================== @@ -406,14 +406,14 @@ export default xoaUpdater export const connectStore = store => { forEach(states, state => - xoaUpdater.on(state, () => store.dispatch(xoaUpdaterState(state))) + xoaUpdater.on(state, () => store.dispatch(setXoaUpdaterState(state))) ) - xoaUpdater.on('trialState', state => store.dispatch(xoaTrialState(state))) - xoaUpdater.on('log', log => store.dispatch(xoaUpdaterLog(log))) + xoaUpdater.on('trialState', state => store.dispatch(setXoaTrialState(state))) + xoaUpdater.on('log', log => store.dispatch(setXoaUpdaterLog(log))) xoaUpdater.on('registerState', registration => - store.dispatch(xoaRegisterState(registration)) + store.dispatch(setXoaRegisterState(registration)) ) xoaUpdater.on('configuration', configuration => - store.dispatch(xoaConfiguration(configuration)) + store.dispatch(setXoaConfiguration(configuration)) ) } diff --git a/packages/xo-web/src/xo-app/xoa/update/index.js b/packages/xo-web/src/xo-app/xoa/update/index.js index e21242289..ba0dfca3c 100644 --- a/packages/xo-web/src/xo-app/xoa/update/index.js +++ b/packages/xo-web/src/xo-app/xoa/update/index.js @@ -2,19 +2,21 @@ import _, { messages } from 'intl' import ActionButton from 'action-button' import AnsiUp from 'ansi_up' import Button from 'button' -import Component from 'base-component' +import decorate from 'apply-decorators' +import defined from '@xen-orchestra/defined' import Icon from 'icon' import React from 'react' import Tooltip from 'tooltip' import xoaUpdater, { exposeTrial, isTrialRunning } from 'xoa-updater' import { addSubscriptions, connectStore } from 'utils' -import { assign, includes, isEmpty, map, some } from 'lodash' import { Card, CardBlock, CardHeader } from 'card' import { confirm } from 'modal' import { Container, Row, Col } from 'grid' -import { createSelector } from 'selectors' import { error } from 'notification' import { injectIntl } from 'react-intl' +import { injectState, provideState } from 'reaclette' +import { isEmpty, map, pick, some, zipObject } from 'lodash' +import { linkState, toggleState } from 'reaclette-utils' import { Password } from 'form' import { serverVersion, subscribeBackupNgJobs, subscribeJobs } from 'xo' @@ -22,472 +24,450 @@ import pkg from '../../../../package' const ansiUp = new AnsiUp() -let updateSource -const promptForReload = (source, force) => { - if (force || (updateSource && source !== updateSource)) { - confirm({ - title: _('promptUpgradeReloadTitle'), - body:

{_('promptUpgradeReloadMessage')}

, - }).then(() => window.location.reload()) - } - updateSource = source -} - if (+process.env.XOA_PLAN < 5) { xoaUpdater.start() + + let updateSource + const promptForReload = (source, force) => { + if (force || (updateSource && source !== updateSource)) { + confirm({ + title: _('promptUpgradeReloadTitle'), + body:

{_('promptUpgradeReloadMessage')}

, + }).then(() => window.location.reload()) + } + updateSource = source + } + xoaUpdater.on('upgradeSuccessful', source => promptForReload(source, !source)) xoaUpdater.on('upToDate', promptForReload) } // FIXME: can't translate -const states = { +const LABELS_BY_STATE = { disconnected: 'Disconnected', + error: 'An error occured', + registerNeeded: 'Registration required', updating: 'Updating', + upgradeNeeded: 'Upgrade required', upgrading: 'Upgrading', upToDate: 'Up to Date', - upgradeNeeded: 'Upgrade required', - registerNeeded: 'Registration required', - error: 'An error occured', } -const update = () => xoaUpdater.update() -const upgrade = ({ runningJobsExist }) => - runningJobsExist - ? confirm({ - title: _('upgradeWarningTitle'), - body: _('upgradeWarningMessage'), - }).then(() => xoaUpdater.upgrade()) - : xoaUpdater.upgrade() +const LEVELS_TO_CLASSES = { + info: 'text-info', + success: 'text-success', + warning: 'text-warning', + error: 'text-danger', +} -@addSubscriptions({ - backupNgJobs: subscribeBackupNgJobs, - jobs: subscribeJobs, -}) -@connectStore(state => { - return { - configuration: state.xoaConfiguration, - log: state.xoaUpdaterLog, - registration: state.xoaRegisterState, - state: state.xoaUpdaterState, - trial: state.xoaTrialState, - } -}) -@injectIntl -export default class XoaUpdates extends Component { - // These 3 inputs are "controlled" http://facebook.github.io/react/docs/forms.html#controlled-components - _handleProxyHostChange = event => - this.setState({ proxyHost: event.target.value || '' }) - _handleProxyPortChange = event => - this.setState({ proxyPort: event.target.value || '' }) - _handleProxyUserChange = event => - this.setState({ proxyUser: event.target.value || '' }) +const PROXY_ENTRIES = ['proxyHost', 'proxyPassword', 'proxyPort', 'proxyUser'] +const initialProxyState = () => zipObject(PROXY_ENTRIES) - _handleConfigReset = () => { - const { configuration } = this.props - const { proxyPassword } = this.refs - proxyPassword.value = '' - this.setState(configuration) - } +const REGISTRATION_ENTRIES = ['email', 'password'] +const initialRegistrationState = () => zipObject(REGISTRATION_ENTRIES) - _register = async () => { - const { email, password } = this.state +const helper = (obj1, obj2, prop) => + defined(() => obj1[prop], () => obj2[prop], '') - const { registration } = this.props - const alreadyRegistered = registration.state === 'registered' +const Updates = decorate([ + addSubscriptions({ + backupNgJobs: subscribeBackupNgJobs, + jobs: subscribeJobs, + }), + connectStore([ + 'xoaConfiguration', + 'xoaRegisterState', + 'xoaTrialState', + 'xoaUpdaterLog', + 'xoaUpdaterState', + ]), + provideState({ + initialState: () => ({ + ...initialProxyState(), + ...initialRegistrationState(), + askRegisterAgain: false, + }), + effects: { + async configure () { + await xoaUpdater.configure( + pick(this.state, [ + 'proxyHost', + 'proxyPassword', + 'proxyPort', + 'proxyUser', + ]) + ) + return this.effects.resetProxyConfig() + }, + initialize () { + return this.effects.update() + }, + linkState, + async register () { + const { state } = this - if (alreadyRegistered) { - try { - await confirm({ - title: _('alreadyRegisteredModal'), - body: ( -

- {_('alreadyRegisteredModalText', { email: registration.email })} -

- ), - }) - } catch (error) { - return - } - } - this.setState({ askRegisterAgain: false }) - return xoaUpdater - .register(email, password, alreadyRegistered) - .then(() => this.setState({ email: '', password: '' })) - } - - _configure = async () => { - const { proxyHost, proxyPort, proxyUser } = this.state - const { proxyPassword } = this.refs - return xoaUpdater - .configure({ - proxyHost, - proxyPort, - proxyUser, - proxyPassword: proxyPassword.value, - }) - .then(config => { - this.setState({ - proxyHost: undefined, - proxyPort: undefined, - proxyUser: undefined, - }) - proxyPassword.value = '' - }) - } - - _trialAllowed = trial => trial.state === 'default' && exposeTrial(trial.trial) - _trialAvailable = trial => - trial.state === 'default' && isTrialRunning(trial.trial) - _trialConsumed = trial => - trial.state === 'default' && - !isTrialRunning(trial.trial) && - !exposeTrial(trial.trial) - _updaterDown = trial => isEmpty(trial) || trial.state === 'ERROR' - _toggleAskRegisterAgain = () => - this.setState({ askRegisterAgain: !this.state.askRegisterAgain }) - - _startTrial = async () => { - try { - await confirm({ - title: _('trialReadyModal'), - body:

{_('trialReadyModalText')}

, - }) - return xoaUpdater - .requestTrial() - .then(() => xoaUpdater.update()) - .catch(err => error('Request Trial', err.message || String(err))) - } catch (_) {} - } - - componentWillMount () { - this.setState({ askRegisterAgain: false }) - serverVersion.then(serverVersion => { - this.setState({ serverVersion }) - }) - update() - } - - _getRunningJobsExist = createSelector( - () => this.props.jobs, - () => this.props.backupNgJobs, - (jobs, backupNgJobs) => - jobs !== undefined && - backupNgJobs !== undefined && - some(jobs.concat(backupNgJobs), job => job.runId !== undefined) - ) - - render () { - const textClasses = { - info: 'text-info', - success: 'text-success', - warning: 'text-warning', - error: 'text-danger', - } - - const { log, registration, state, trial } = this.props - let { configuration } = this.props // Configuration from the store - - const alreadyRegistered = registration.state === 'registered' - - configuration = assign({}, configuration) - const { proxyHost, proxyPort, proxyUser } = this.state // Edited non-saved configuration values override in view - let configEdited = false - proxyHost !== undefined && - (configuration.proxyHost = proxyHost) && - (configEdited = true) - proxyPort !== undefined && - (configuration.proxyPort = proxyPort) && - (configEdited = true) - proxyUser !== undefined && - (configuration.proxyUser = proxyUser) && - (configEdited = true) - - const { formatMessage } = this.props.intl - return ( - - - - - - {states[state]} - - + const { isRegistered } = state + if (isRegistered) { + try { + await confirm({ + title: _('alreadyRegisteredModal'), + body: (

- {_('currentVersion')}{' '} - {`xo-server ${this.state.serverVersion}`} /{' '} - {`xo-web ${pkg.version}`} + {_('alreadyRegisteredModalText', { + email: this.props.registration.email, + })}

- {includes(['error', 'disconnected'], state) && ( -

- - {_('updaterTroubleshootingLink')} - + ), + }) + } catch (_) { + return + } + } + + state.askRegisterAgain = false + const { email, password } = state + await xoaUpdater.register(email, password, isRegistered) + + return initialRegistrationState() + }, + resetProxyConfig: initialProxyState, + async startTrial () { + try { + await confirm({ + title: _('trialReadyModal'), + body:

{_('trialReadyModalText')}

, + }) + } catch (_) { + return + } + try { + await xoaUpdater.requestTrial() + await xoaUpdater.update() + } catch (err) { + error('Request Trial', err.message || String(err)) + } + }, + toggleState, + update: () => xoaUpdater.update(), + upgrade: () => xoaUpdater.upgrade(), + }, + computed: { + areJobsRunning: (_, { jobs, backupNgJobs }) => + jobs !== undefined && + backupNgJobs !== undefined && + some(jobs.concat(backupNgJobs), job => job.runId !== undefined), + isDisconnected: (_, { xoaUpdaterState }) => + xoaUpdater === 'disconnected' || xoaUpdaterState === 'error', + isProxyConfigEdited: state => + PROXY_ENTRIES.some(entry => state[entry] !== undefined), + isRegistered: (_, { xoaRegisterState }) => + xoaRegisterState.state === 'register', + isTrialAllowed: (_, { xoaTrialState }) => + xoaTrialState.state === 'default' && exposeTrial(xoaTrialState.trial), + isTrialAvailable: (_, { xoaTrialState }) => + xoaTrialState.state === 'default' && + isTrialRunning(xoaTrialState.trial), + isTrialConsumed: (_, { xoaTrialState }) => + xoaTrialState.state === 'default' && + !isTrialRunning(xoaTrialState.trial) && + !exposeTrial(xoaTrialState.trial), + isUpdaterDown: (_, { xoaTrialState }) => + isEmpty(xoaTrialState) || xoaTrialState.state === 'ERROR', + serverVersion: () => serverVersion, + }, + }), + injectState, + injectIntl, + ({ + effects, + intl: { formatMessage }, + state, + xoaConfiguration, + xoaRegisterState, + xoaTrialState, + xoaUpdaterLog, + xoaUpdaterState, + }) => ( + + + + + + {LABELS_BY_STATE[xoaUpdaterState]} + + +

+ {_('currentVersion')} {`xo-server ${state.serverVersion}`} /{' '} + {`xo-web ${pkg.version}`} +

+ {state.isDisconnected && ( +

+ + {_('updaterTroubleshootingLink')} + +

+ )} + + {_('refresh')} + {' '} + + {xoaTrialState.state !== 'untrustedTrial' + ? _('upgrade') + : _('downgrade')} + +
+
+ {map(xoaUpdaterLog, (log, key) => ( +

+ + {log.date} + + :{' '} +

- )} - - {_('refresh')} - {' '} - - {trial.state !== 'untrustedTrial' - ? _('upgrade') - : _('downgrade')} - -
-
- {map(log, (log, key) => ( -

- {log.date} - :{' '} - -

- ))} -
- - - - - - - - - {_('proxySettings')} {configEdited ? '*' : ''} - - -
-
-
- -
{' '} -
- -
{' '} -
- -
{' '} -
- -
-
-
-
- - {_('saveResourceSet')} - {' '} - -
-
-
-
- - - - {_('registration')} - - {registration.state} - {registration.email && to {registration.email}} - {registration.error} - {!alreadyRegistered || this.state.askRegisterAgain ? ( -
-
- -
{' '} -
- -
{' '} - - {_('register')} - -
- ) : ( - - {_('editRegistration')} - - )} - {+process.env.XOA_PLAN === 1 && ( -
-

{_('trial')}

- {this._trialAllowed(trial) && ( -
- {registration.state !== 'registered' && ( -

{_('trialRegistration')}

- )} - {registration.state === 'registered' && ( - - {_('trialStartButton')} - - )} -
- )} - {this._trialAvailable(trial) && ( -

- {_('trialAvailableUntil', { - date: new Date(trial.trial.end), - })} -

- )} - {this._trialConsumed(trial) &&

{_('trialConsumed')}

} + ))} +
+
+
+ +
+ + + + + {_('proxySettings')} {state.isProxyConfigEdited ? '*' : ''} + + +
+
+
+ +
{' '} +
+ +
{' '} +
+ +
{' '} +
+
- )} - {process.env.XOA_PLAN > 1 && - process.env.XOA_PLAN < 5 && ( +
+
+
+ + {_('formSave')} + {' '} + +
+
+
+
+ + + + {_('registration')} + + {xoaRegisterState.state} + {xoaRegisterState.email && ( + to {xoaRegisterState.email} + )} + {xoaRegisterState.error} + {!state.isRegistered || state.askRegisterAgain ? ( +
+
+ +
{' '} +
+ +
{' '} + + {_('register')} + +
+ ) : ( + + {_('editRegistration')} + + )} + {+process.env.XOA_PLAN === 1 && ( +
+

{_('trial')}

+ {state.isTrialAllowed && (
- {trial.state === 'trustedTrial' &&

{trial.message}

} - {trial.state === 'untrustedTrial' && ( -

{trial.message}

+ {state.isRegistered ? ( + + {_('trialStartButton')} + + ) : ( +

{_('trialRegistration')}

)}
)} - {process.env.XOA_PLAN < 5 && ( + {state.isTrialAvailable && ( +

+ {_('trialAvailableUntil', { + date: new Date(xoaTrialState.trial.end), + })} +

+ )} + {state.isTrialConsumed &&

{_('trialConsumed')}

} +
+ )} + {process.env.XOA_PLAN > 1 && + process.env.XOA_PLAN < 5 && (
- {this._updaterDown(trial) && ( -

{_('trialLocked')}

+ {xoaTrialState.state === 'trustedTrial' && ( +

{xoaTrialState.message}

+ )} + {xoaTrialState.state === 'untrustedTrial' && ( +

{xoaTrialState.message}

)}
)} -
-
- -
- - ) - } + {process.env.XOA_PLAN < 5 && ( +
+ {state.isUpdaterDown && ( +

{_('trialLocked')}

+ )} +
+ )} + + + + + + ), +]) +export { Updates as default } + +const COMPONENTS_BY_STATE = { + connected: ( + + + + + ), + disconnected: ( + + + + + ), + error: ( + + + + + ), + registerNeeded: , + upgradeNeeded: ( + + + + + ), + upToDate: , +} +const TOOLTIPS_BY_STATE = { + connected: _('waitingUpdateInfo'), + disconnected: _('noUpdateInfo'), + error: _('updaterError'), + registerNeeded: _('registerNeeded'), + upgradeNeeded: _('mustUpgrade'), + upToDate: _('upToDate'), } -const UpdateAlarm = () => ( - - - - -) - -const UpdateError = () => ( - - - - -) - -const UpdateWarning = () => ( - - - - -) - -const UpdateSuccess = () => - -const UpdateAlert = () => ( - - - - -) - -const RegisterAlarm = () => ( - -) - -export const UpdateTag = connectStore(state => { - return { - configuration: state.xoaConfiguration, - log: state.xoaUpdaterLog, - registration: state.xoaRegisterState, - state: state.xoaUpdaterState, - trial: state.xoaTrialState, - } -})(props => { - const { state } = props - const components = { - disconnected: , - connected: , - upToDate: , - upgradeNeeded: , - registerNeeded: , - error: , - } - const tooltips = { - disconnected: _('noUpdateInfo'), - connected: _('waitingUpdateInfo'), - upToDate: _('upToDate'), - upgradeNeeded: _('mustUpgrade'), - registerNeeded: _('registerNeeded'), - error: _('updaterError'), - } - return {components[state]} -}) +export const UpdateTag = connectStore(state => ({ + state: state.xoaUpdaterState, +}))(({ state }) => ( + + {COMPONENTS_BY_STATE[state]} + +)) diff --git a/yarn.lock b/yarn.lock index 116f29ab5..cf371feae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10797,10 +10797,10 @@ rc@^1.1.6, rc@^1.1.7, rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -reaclette@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/reaclette/-/reaclette-0.6.0.tgz#7f3d84d51c9461dde7d92e445fa48a43d728967c" - integrity sha512-GkUENuI56UTXboaGHIjlVc/FIGeX4GaIaVguKH3kiGJOevDbwicTuZHF/86cpIktNRJY4u2SI8tH+ygJC1v+wg== +reaclette@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/reaclette/-/reaclette-0.7.0.tgz#ff69aacb2c18747630f386f4627771e9fe584d54" + integrity sha512-6mk0s9u9hmLsrSLuOdmzaBazUI8bLbrptiXqe8pqYJiw4Gw1wtMhFkt/Qvu1way8qykq7h54V/TDvAuSzH5AKA== react-addons-shallow-compare@^15.0.2, react-addons-shallow-compare@^15.6.2: version "15.6.2"