diff --git a/src/actions/options.js b/src/actions/options.js index f26838c84c..e7cda1825e 100644 --- a/src/actions/options.js +++ b/src/actions/options.js @@ -2,16 +2,12 @@ import { GET_SSH_KEY, SAVE_OPTION, SAVE_GLOBAL_OPTIONS, - SAVE_OPTION_TO_VMS, SAVE_SSH_KEY, - SAVE_VM_OPTIONS, + SAVE_VMS_OPTIONS, SET_SSH_KEY, SET_OPTION, SET_OPTION_TO_VMS, SET_OPTIONS_SAVE_RESULTS, - RESET_GLOBAL_SETTINGS, - RESET_OPTIONS, - RESET_VM_SETTINGS, } from '_/constants' export function getSSHKey ({ userId }) { @@ -67,12 +63,11 @@ export function saveOption ({ key, value, vmId }) { } } -export function saveGlobalOptions ({ values, checkedVms }, { correlationId }) { +export function saveGlobalOptions ({ values }, { correlationId }) { return { type: SAVE_GLOBAL_OPTIONS, payload: { values, - checkedVms, }, meta: { correlationId, @@ -80,12 +75,12 @@ export function saveGlobalOptions ({ values, checkedVms }, { correlationId }) { } } -export function saveVmOptions ({ values, vmId }, { correlationId }) { +export function saveVmsOptions ({ values, vmIds }, { correlationId }) { return { - type: SAVE_VM_OPTIONS, + type: SAVE_VMS_OPTIONS, payload: { values, - vmId, + vmIds, }, meta: { correlationId, @@ -104,42 +99,6 @@ export function setOptionsSaveResults ({ correlationId, status, details }) { } } -export function saveOptionToVms ({ key, value, vmIds, values }) { - return { - type: SAVE_OPTION_TO_VMS, - payload: { - key, - value, - vmIds, - values, - }, - } -} - -export function resetGlobalSettings () { - return { - type: RESET_GLOBAL_SETTINGS, - } -} - -export function resetVmSettings ({ vmId }) { - return { - type: RESET_VM_SETTINGS, - payload: { - vmId, - }, - } -} - -export function resetOptions ({ vmId } = {}) { - return { - type: RESET_OPTIONS, - payload: { - vmId, - }, - } -} - export function saveSSHKey ({ key, userId, sshId }) { return { type: SAVE_SSH_KEY, diff --git a/src/components/Pages/index.js b/src/components/Pages/index.js index 3aff9d824f..334016544c 100644 --- a/src/components/Pages/index.js +++ b/src/components/Pages/index.js @@ -32,14 +32,14 @@ class VmSettingsPage extends React.Component { constructor (props) { super(props) this.state = { - vmId: undefined, + vmIds: [], } } static getDerivedStateFromProps (props, state) { - if (state.vmId !== props.match.params.id) { - const vmId = props.match.params.id - return { vmId } + const ids = props.match.params.id.split('/') + if (ids.filter(n => !state.vmIds.includes(n)).length > 0) { + return { vmIds: ids } } return null @@ -47,14 +47,14 @@ class VmSettingsPage extends React.Component { render () { const { vms } = this.props - const { vmId } = this.state + const { vmIds } = this.state - if (vmId && vms.getIn(['vms', vmId])) { - return () + if (vmIds.length > 0 && vmIds.filter(n => !vms.get('vms').keySeq().includes(n)).length === 0) { + return () } // TODO: Add handling for if the fetch runs but fails (FETCH-FAIL), see issue #631 - console.info(`VmSettingsPage: VM id cannot be found: ${vmId}`) + console.info(`VmSettingsPage: VM id cannot be found: ${vmIds}`) return null } } diff --git a/src/components/UserSettings/CounterAlert.js b/src/components/Settings/CounterAlert.js similarity index 100% rename from src/components/UserSettings/CounterAlert.js rename to src/components/Settings/CounterAlert.js diff --git a/src/components/Settings/SettingsBase.js b/src/components/Settings/SettingsBase.js new file mode 100644 index 0000000000..6d260e88ad --- /dev/null +++ b/src/components/Settings/SettingsBase.js @@ -0,0 +1,83 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { + Card, + Col, + ControlLabel, + FormGroup, + FieldLevelHelp, +} from 'patternfly-react' + +import style from './style.css' + +const LabelCol = ({ children, tooltip, ...props }) => { + return + { children } { tooltip && } + +} +LabelCol.propTypes = { + children: PropTypes.node.isRequired, + tooltip: PropTypes.string, +} + +const Item = ({ title, isActive, onClick }) => { + return
  • + { e.preventDefault(); onClick() }}> + {title} +
    + +
  • +} + +Item.propTypes = { + title: PropTypes.string.isRequired, + isActive: PropTypes.bool, + onClick: PropTypes.func.isRequired, +} + +const Section = ({ name, section }) => ( + +

    {section.title} { section.tooltip && }

    + { section.fields.map((field) => ( + + + { field.title } + + + {field.body} + + + )) } +
    +) + +Section.propTypes = { + name: PropTypes.string.isRequired, + section: PropTypes.object.isRequired, +} + +class SettingsBase extends Component { + buildSection (key, section) { + return ( + +
    +
    +
    +
    + ) + } + + render () { + const { sections } = this.props + return ( +
    + {Object.entries(sections).filter(([key, section]) => !!section).map(([key, section]) => this.buildSection(key, section))} +
    + ) + } +} +SettingsBase.propTypes = { + sections: PropTypes.object.isRequired, +} + +export default SettingsBase diff --git a/src/components/Settings/SettingsToolbar.js b/src/components/Settings/SettingsToolbar.js new file mode 100644 index 0000000000..aa19fa5e49 --- /dev/null +++ b/src/components/Settings/SettingsToolbar.js @@ -0,0 +1,58 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import PropTypes from 'prop-types' +import { Toolbar } from 'patternfly-react' +import { msg } from '_/intl' + +import style from './style.css' + +class SettingsToolbar extends React.Component { + constructor (props) { + super(props) + this.el = document.createElement('div') + } + + componentDidMount () { + const root = document.getElementById('settings-toolbar') + if (root) { + root.appendChild(this.el) + } + } + + componentWillUnmount () { + const root = document.getElementById('settings-toolbar') + if (root) { + root.removeChild(this.el) + } + } + render () { + const { onSave, onCancel } = this.props + const body = + + + + + + return ReactDOM.createPortal( + body, + this.el + ) + } +} + +SettingsToolbar.propTypes = { + onSave: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, +} + +export default SettingsToolbar diff --git a/src/components/Settings/index.js b/src/components/Settings/index.js index a2fe9e20f4..9ff4ca19b9 100644 --- a/src/components/Settings/index.js +++ b/src/components/Settings/index.js @@ -1,153 +1,123 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import { - Card, - TypeAheadSelect, - Col, - ControlLabel, - FormGroup, - FieldLevelHelp, -} from 'patternfly-react' +import { connect } from 'react-redux' -import style from './style.css' - -const LabelCol = ({ children, tooltip, ...props }) => { - return - { children } { tooltip && } - -} -LabelCol.propTypes = { - children: PropTypes.node.isRequired, - tooltip: PropTypes.string, -} - -const Item = ({ title, isActive, onClick }) => { - return
  • - { e.preventDefault(); onClick() }}> - {title} -
    - -
  • -} - -Item.propTypes = { - title: PropTypes.string.isRequired, - isActive: PropTypes.bool, - onClick: PropTypes.func.isRequired, -} - -const Section = ({ section }) => ( - -

    {section.title} { section.tooltip && }

    - { section.fields.map((field) => ( - - - { field.title } - - - {field.body} - - - )) } -
    -) +import SettingsBase from './SettingsBase' +import SettingsToolbar from './SettingsToolbar' +import NavigationPrompt from 'react-router-navigation-prompt' +import NavigationConfirmationModal from '../NavigationConfirmationModal' +import CounterAlert from './CounterAlert' +import { generateUnique } from '_/helpers' +import { msg } from '_/intl' -Section.propTypes = { - section: PropTypes.object.isRequired, -} +import style from './style.css' class Settings extends Component { constructor (props) { super(props) this.state = { - selectedSection: Object.keys(props.sections)[0], + saved: false, + errors: null, + changed: false, + changedFields: new Set(), + correlationId: null, } - this.handleSectionChange = this.handleSectionChange.bind(this) - this.buildItems = this.buildItems.bind(this) - this.handleSearch = this.handleSearch.bind(this) + this.handleChange = this.handleChange.bind(this) + this.handleSave = this.handleSave.bind(this) + this.handleSaveNotificationDissmised = this.handleSaveNotificationDissmised.bind(this) + this.handleErrorNotificationDissmised = this.handleErrorNotificationDissmised.bind(this) } - handleSectionChange (section) { - this.setState({ selectedSection: section }) + handleSave () { + const { values, onSave } = this.props + const { changedFields } = this.state + const saveFields = [...changedFields].reduce((acc, cur) => ({ ...acc, [cur]: values[cur] }), {}) + this.setState({ correlationId: generateUnique('VmSettings-save_') }, () => onSave(saveFields, this.state.correlationId)) } - buildItems () { - const { sections } = this.props - const sectionsItems = [] - for (let i in sections) { - if (sections[i]) { - const handleClick = (() => this.handleSectionChange.bind(this, i))() - sectionsItems.push() + static getDerivedStateFromProps (props, state) { + const res = props.options.getIn(['results', state.correlationId]) + if (state.correlationId !== null && res) { + if (res.status === 'OK') { + return { + correlationId: null, + saved: true, + changed: false, + } + } + if (res.status === 'ERROR' && res.details) { + return { + corellationId: null, + errors: res.details, + } } } - return sectionsItems + return null } - handleSearch (options) { - const { sections } = this.props - const option = options[0] - for (let i in sections) { - const fields = Array.isArray(sections[i]) ? sections[i].reduce((a, v) => [...a, ...v.fields], []) : sections[i].fields - if (fields.find(v => v.title === option) !== undefined) { - this.handleSectionChange(i) - break - } - } + handleSaveNotificationDissmised () { + this.setState({ saved: false }) } - buildSection (section) { - if (Array.isArray(section)) { - return
    - { section.map(s => ( -
    - ))} -
    + + handleErrorNotificationDissmised () { + this.setState({ errors: null }) + } + + handleChange (field, params) { + const { mapper, onChange } = this.props + return (value) => { + const v = typeof value === 'object' ? Object.assign({}, value) : value + this.setState((state) => { + const { changedFields } = this.state + const changedFieldsClone = changedFields.add(field) + return { changed: true, saved: false, changedFields: changedFieldsClone } + }, () => { + const values = { ...this.props.values } + values[field] = mapper[field](v, values[field], params) + onChange(values) + }) } - return ( -
    -
    -
    - ) } render () { - const { sections } = this.props - const allOptionsTitle = Object.values(sections).reduce((acc, val) => { - if (!val) { - return acc - } - const fields = Array.isArray(val) ? val.reduce((a, v) => [...a, ...v.fields], []) : val.fields - return fields.reduce((acc2, val2) => ([ ...acc2, val2.title ]), acc) - }, []) - return ( -
    - -
      - {this.buildItems()} -
    -
    -
    - - - {this.buildSection(sections[this.state.selectedSection])} - -
    -
    - ) + const { buildSections, onCancel } = this.props + return + + {({ isActive, onConfirm, onCancel }) => ( + + )} + + { this.state.saved &&
    + +
    } + { this.state.errors &&
    + msg[e]()).join(', '), + }) + } + onDismiss={this.handleErrorNotificationDissmised} + /> +
    } + + +
    } } Settings.propTypes = { - sections: PropTypes.object.isRequired, + mapper: PropTypes.object.isRequired, + values: PropTypes.object.isRequired, + options: PropTypes.object.isRequired, + buildSections: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + onSave: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, } -export default Settings +export default connect( + (state) => ({ + options: state.options, + }), +)(Settings) diff --git a/src/components/Settings/style.css b/src/components/Settings/style.css index d37f89d5f6..a6e68a13f3 100644 --- a/src/components/Settings/style.css +++ b/src/components/Settings/style.css @@ -11,6 +11,7 @@ .search-content-box { flex-grow: 1; + height: min-content; } .main-content { @@ -36,4 +37,24 @@ padding-left: 20px; padding-right: 20px; margin-bottom: 0; -} \ No newline at end of file +} + +.alert-container { + position: absolute; + top: 5%; + left: 25%; + width: 50%; +} + +.toolbar :global(.toolbar-pf.row) { + padding-bottom: 10px; + margin-left: 0; +} + +.toolbar :global(.toolbar-pf-action-right) button { + margin-right: 10px; +} + +:global(#settings-toolbar) { + margin-left: -20px; +} diff --git a/src/components/Toolbar/VmsListToolbar.js b/src/components/Toolbar/VmsListToolbar.js index 2e28aa2aa2..017ae8c205 100644 --- a/src/components/Toolbar/VmsListToolbar.js +++ b/src/components/Toolbar/VmsListToolbar.js @@ -62,7 +62,9 @@ const VmsListToolbar = ({ match, vms, onRemoveFilter, onClearFilters }) => { +
    +
    diff --git a/src/components/Toolbar/index.js b/src/components/Toolbar/index.js index 57b85d952d..463dd010ba 100644 --- a/src/components/Toolbar/index.js +++ b/src/components/Toolbar/index.js @@ -2,8 +2,11 @@ import React from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' -import { Toolbar } from 'patternfly-react' +import { Toolbar, Icon } from 'patternfly-react' +import { Link } from 'react-router-dom' +import { msg } from '_/intl' import style from './style.css' +import sharedStyle from '_/components/sharedStyle.css' import { RouterPropTypeShapes } from '_/propTypeShapes' import VmActions from '../VmActions' import VmConsoleSelector from '../VmConsole/VmConsoleSelector' @@ -75,6 +78,13 @@ const VmConsoleToolbar = ({ match, vms, consoles }) => { disabled={!consoleStatus.includes(consoles.getIn(['vms', match.params.id, 'consoleStatus']))} />
    + + + {msg.consoleSettings()} +
    diff --git a/src/components/Toolbar/style.css b/src/components/Toolbar/style.css index 9c47286f68..b63d6318f7 100644 --- a/src/components/Toolbar/style.css +++ b/src/components/Toolbar/style.css @@ -29,3 +29,8 @@ overflow: auto; max-height: 600px; } + +:global(#select-all-vms-btn-box) { + margin-left: 10px; + display: inline-block; +} diff --git a/src/components/UserSettings/GlobalSettings.js b/src/components/UserSettings/GlobalSettings.js index 261c529348..9a0bac96ad 100644 --- a/src/components/UserSettings/GlobalSettings.js +++ b/src/components/UserSettings/GlobalSettings.js @@ -1,26 +1,14 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import Immutable from 'immutable' import { connect } from 'react-redux' import { push } from 'connected-react-router' -import { resetGlobalSettings, saveGlobalOptions } from '_/actions' -import { FormControl, Checkbox, Switch } from 'patternfly-react' +import { saveGlobalOptions } from '_/actions' +import { FormControl, Switch } from 'patternfly-react' import { localeWithFullName, locale, msg } from '_/intl' -import { generateUnique } from '_/helpers' import Settings from '../Settings' import SelectBox from '../SelectBox' -import NavigationPrompt from 'react-router-navigation-prompt' -import NavigationConfirmationModal from '../NavigationConfirmationModal' -import VmsNotificationsList from './VmsNotificationsList' -import SettingsToolbar from './SettingsToolbar' -import style from './style.css' -import CounterAlert from './CounterAlert' -import SaveConfirmationModal from './SaveConfirmationModal' -import ResetConfirmationModal from './ResetConfirmationModal' - -const EMPTY_MAP = Immutable.fromJS({}) const valuesMapper = { 'language': (value) => value, @@ -36,13 +24,6 @@ const valuesMapper = { 'confirmVmSuspending': (e) => e.target.checked, 'autoConnect': (e) => e.target.checked, 'smartcard': (e) => e.target.checked, - 'allVmsNotifications': (value) => value, - 'vmsNotifications': (e, prevValue, params) => { - if (params && params.vmId) { - return Object.assign({}, prevValue, { [params.vmId]: e.target.checked }) - } - return e - }, } const dontDisturbList = [ @@ -86,151 +67,50 @@ const updateRateList = [ class GlobalSettings extends Component { constructor (props) { super(props) - const vmsNotifications = props.options.get('vms', EMPTY_MAP).map(v => v.get('notifications', null)).filter(v => v !== null).toJS() this.state = { values: { sshKey: props.options.getIn(['options', 'ssh', 'key']), language: props.options.getIn(['options', 'language']) || locale, dontDisturb: props.options.getIn(['options', 'dontDisturb']) || false, dontDisturbFor: props.options.getIn(['options', 'dontDisturbFor']) || dontDisturbList[0].id, - vmsNotifications: vmsNotifications || {}, - allVmsNotifications: props.options.getIn(['options', 'allVmsNotifications']) || false, updateRate: props.options.getIn(['options', 'updateRate']) || 60, - autoConnect: props.options.getIn(['options', 'autoConnect']) || false, - displayUnsavedWarnings: props.options.getIn(['options', 'displayUnsavedWarnings'], true), - confirmForceShutdown: props.options.getIn(['options', 'confirmForceShutdown'], true), - confirmVmDeleting: props.options.getIn(['options', 'confirmVmDeleting'], true), - confirmVmSuspending: props.options.getIn(['options', 'confirmVmSuspending'], true), - ctrlAltDel: props.options.getIn(['options', 'ctrlAltDel']) || false, - smartcard: props.options.getIn(['options', 'smartcard']) || false, - fullScreenMode: props.options.getIn(['options', 'fullScreenMode']) || false, }, - saved: false, - errors: null, - changed: false, - updateValues: false, - showSaveConfirmation: false, - showResetConfirmation: false, - changedFields: new Set(), - correlationId: null, } this.handleChange = this.handleChange.bind(this) - this.handleSave = this.handleSave.bind(this) this.handleCancel = this.handleCancel.bind(this) - this.handleReset = this.handleReset.bind(this) - this.handleSaveNotificationDissmised = this.handleSaveNotificationDissmised.bind(this) - this.handleErrorNotificationDissmised = this.handleErrorNotificationDissmised.bind(this) - this.handleResetConfirmation = this.handleResetConfirmation.bind(this) - } - - static getDerivedStateFromProps (props, state) { - if (state.updateValues) { - const vmsNotifications = props.options.get('vms', EMPTY_MAP).map(v => v.get('notifications', null)).filter(v => v !== null).toJS() - return { - values: { - sshKey: props.options.getIn(['options', 'ssh', 'key']), - language: props.options.getIn(['options', 'language']) || locale, - dontDisturb: props.options.getIn(['options', 'dontDisturb']) || false, - dontDisturbFor: props.options.getIn(['options', 'dontDisturbFor']) || dontDisturbList[0].id, - vmsNotifications: vmsNotifications || {}, - allVmsNotifications: props.options.getIn(['options', 'allVmsNotifications']) || false, - updateRate: props.options.getIn(['options', 'updateRate']) || 60, - autoConnect: props.options.getIn(['options', 'autoConnect']) || false, - displayUnsavedWarnings: props.options.getIn(['options', 'displayUnsavedWarnings'], true), - confirmForceShutdown: props.options.getIn(['options', 'confirmForceShutdown'], true), - confirmVmDeleting: props.options.getIn(['options', 'confirmVmDeleting'], true), - confirmVmSuspending: props.options.getIn(['options', 'confirmVmSuspending'], true), - ctrlAltDel: props.options.getIn(['options', 'ctrlAltDel']) || false, - smartcard: props.options.getIn(['options', 'smartcard']) || false, - fullScreenMode: props.options.getIn(['options', 'fullScreenMode']) || false, - }, - updateValues: false, - } - } - const res = props.options.getIn(['results', state.correlationId]) - if (state.correlationId !== null && res) { - if (res.status === 'OK') { - if (state.changedFields.has('language')) { - document.location.href = '/settings' - } - return { - correlationId: null, - saved: true, - changed: false, - changedFields: new Set(), - } - } - if (res.status === 'ERROR' && res.details) { - return { - correlationId: null, - errors: res.details, - changedFields: new Set(), - } - } - } - return null + this.buildSections = this.buildSections.bind(this) + this.updateSshKey = this.updateSshKey.bind(this) } - handleSave (avoidConfirmation = false, checkedVms) { - const { options, saveOptions } = this.props - if (options.get('vms').size > 0 && !avoidConfirmation) { - this.setState({ showSaveConfirmation: true }) - } else { - const { values, changedFields } = this.state - const saveFields = [...changedFields].reduce((acc, cur) => ({ ...acc, [cur]: values[cur] }), {}) - this.setState({ correlationId: generateUnique('GlobalSettings-save_') }, () => saveOptions(saveFields, checkedVms, this.state.correlationId)) - } + handleChange (values) { + this.setState({ values }) } - handleChange (field, params) { - return (value, changeState = true) => { - const v = typeof value === 'object' ? Object.assign({}, value) : value - this.setState((state) => { - const { values, changedFields } = this.state - const changedFieldsClone = changedFields.add(field) - values[field] = valuesMapper[field](v, values[field], params) - if (changeState) { - return { values, changed: true, saved: false, changedFields: changedFieldsClone } - } + updateSshKey (prevProps, prevState) { + const { options } = prevProps + const prevSshKey = options.getIn(['options', 'ssh', 'key']) + if (!prevSshKey && prevSshKey !== this.props.options.getIn(['options', 'ssh', 'key']) && prevState.values.sshKey === prevSshKey) { + this.setState(state => { + const values = { ...state.values } + values.sshKey = this.props.options.getIn(['options', 'ssh', 'key']) return { values } }) } } - handleSaveNotificationDissmised () { - this.setState({ saved: false }) - } - - handleErrorNotificationDissmised () { - this.setState({ errors: null }) - } - componentDidUpdate (prevProps, prevState) { - const { options } = prevProps - const prevSshKey = options.getIn(['options', 'ssh', 'key']) - if (!prevSshKey && prevSshKey !== this.props.options.getIn(['options', 'ssh', 'key']) && prevState.values.sshKey === prevSshKey) { - this.handleChange('sshKey')(this.props.options.getIn(['options', 'ssh', 'key']), false) - } + this.updateSshKey(prevProps, prevState) } handleCancel () { this.props.goToMainPage() } - handleResetConfirmation () { - this.setState({ showResetConfirmation: true }) - } - - handleReset () { - this.setState({ saved: true, changed: false, updateValues: true, showResetConfirmation: false }) - this.props.resetSettings() - } - - render () { + buildSections (onChange) { const { config } = this.props const { values } = this.state const idPrefix = 'global-user-settings' - const sections = { + return { general: { title: msg.general(), fields: [ @@ -248,107 +128,39 @@ class GlobalSettings extends Component { id={`${idPrefix}-language`} items={localeWithFullName} selected={values.language} - onChange={this.handleChange('language')} + onChange={onChange('language')} />, }, - ], - }, - vm: [{ - title: msg.virtualMachines(), - tooltip: msg.globalSettingsWillBeApplied(), - fields: [ - { - title: msg.uiRefresh(), - body: , - }, - ], - }, - { - title: msg.confirmationMessages(), - fields: [ - { - title: msg.displayUnsavedChangesWarnings(), - body: - {msg.displayUnsavedChangesWarningsDetail()} - , - }, - { - title: msg.confirmForceShutdowns(), - body: - {msg.confirmForceShutdownsDetails()} - , - }, - { - title: msg.confirmDeletingVm(), - body: - {msg.confirmDeletingVmDetails()} - , - }, - { - title: msg.confirmSuspendingVm(), - body: - {msg.confirmSuspendingVmDetails()} - , - }, - ], - }], - console: { - title: msg.console(), - tooltip: msg.globalSettingsWillBeApplied(), - fields: [ { title: msg.sshKey(), tooltip: msg.sshKeyTooltip(), body: this.handleChange('sshKey')(e.target.value)} + onChange={e => onChange('sshKey')(e.target.value)} value={values.sshKey} />, }, + ], + }, + refreshInterval: { + title: msg.refreshInterval(), + tooltip: msg.globalSettingsWillBeApplied(), + fields: [ { - title: msg.fullScreenMode(), - body: this.handleChange('fullScreenMode')(state)} - />, - }, - { - title: msg.ctrlAltDel(), - tooltip: msg.ctrlAltDelTooltip(), - body: this.handleChange('ctrlAltDel')(state)} + title: msg.uiRefresh(), + body: , }, - { - title: msg.automaticConsoleConnection(), - body: - {msg.automaticConsoleConnectionDetails()} - , - }, - { - title: msg.smartcard(), - tooltip: msg.smartcardTooltip(), - body: - {msg.smartcardDetails()} - , - }, ], }, notifications: { title: msg.notifications(), + tooltip: msg.notificationSettingsAffectAllNotifications(), fields: [ { title: msg.dontDisturb(), @@ -357,7 +169,7 @@ class GlobalSettings extends Component { bsSize='normal' title='normal' value={values.dontDisturb} - onChange={(e, state) => this.handleChange('dontDisturb')(state)} + onChange={(e, state) => onChange('dontDisturb')(state)} />, }, { @@ -366,50 +178,27 @@ class GlobalSettings extends Component { id={`${idPrefix}-dont-disturb-for`} items={dontDisturbList} selected={values.dontDisturbFor} - onChange={this.handleChange('dontDisturbFor')} + onChange={onChange('dontDisturbFor')} disabled={!values.dontDisturb} />, }, - { - title: msg.disableVmNotifications(), - body: , - }, ], }, } + } + + render () { + const { saveOptions } = this.props return (
    - - {({ isActive, onConfirm, onCancel }) => ( - - )} - - { this.state.saved &&
    - -
    } - { this.state.errors &&
    - msg[e]()).join(', '), - }) - } - onDismiss={this.handleErrorNotificationDissmised} - /> -
    } - this.setState({ showSaveConfirmation: false })} - onSave={this.handleSave} - /> - this.setState({ showResetConfirmation: false })} + - this.handleSave()} onCancel={this.handleCancel} onReset={this.handleResetConfirmation} /> -
    ) } @@ -419,7 +208,6 @@ GlobalSettings.propTypes = { options: PropTypes.object.isRequired, saveOptions: PropTypes.func.isRequired, goToMainPage: PropTypes.func.isRequired, - resetSettings: PropTypes.func.isRequired, } export default connect( @@ -429,8 +217,7 @@ export default connect( }), (dispatch) => ({ - saveOptions: (values, checkedVms, correlationId) => dispatch(saveGlobalOptions({ values, checkedVms }, { correlationId })), + saveOptions: (values, correlationId) => dispatch(saveGlobalOptions({ values }, { correlationId })), goToMainPage: () => dispatch(push('/')), - resetSettings: () => dispatch(resetGlobalSettings()), }) )(GlobalSettings) diff --git a/src/components/UserSettings/ResetConfirmationModal.js b/src/components/UserSettings/ResetConfirmationModal.js deleted file mode 100644 index 33f5f0cb61..0000000000 --- a/src/components/UserSettings/ResetConfirmationModal.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -import { Icon } from 'patternfly-react' -import ConfirmationModal from '../VmActions/ConfirmationModal' -import { msg } from '_/intl' - -const ResetConfirmationModal = ({ show, onClose, onConfirm }) => { - return ( - - -
    -

    {msg.areYouSureYouWantToResetSettings()}

    -

    {msg.resettingAccountSettingsWillClearSettings()}

    -
    - - } - confirm={{ title: msg.confirmChanges(), onClick: onConfirm }} - onClose={onClose} - /> - ) -} -ResetConfirmationModal.propTypes = { - show: PropTypes.bool, - onClose: PropTypes.func.isRequired, - onConfirm: PropTypes.func.isRequired, -} - -export default ResetConfirmationModal diff --git a/src/components/UserSettings/SaveConfirmationModal.js b/src/components/UserSettings/SaveConfirmationModal.js index 9dcca4dd3b..4136913e4d 100644 --- a/src/components/UserSettings/SaveConfirmationModal.js +++ b/src/components/UserSettings/SaveConfirmationModal.js @@ -3,7 +3,6 @@ import PropTypes from 'prop-types' import { Icon } from 'patternfly-react' import ConfirmationModal from '../VmActions/ConfirmationModal' -import VmsSettingsList from './VmsSettingsList' import { msg } from '_/intl' class SaveConfirmationModal extends React.Component { @@ -21,7 +20,7 @@ class SaveConfirmationModal extends React.Component { this.setState({ checkedVms }) } render () { - const { show, onClose, onSave } = this.props + const { show, vms, onClose, onConfirm } = this.props return (

    {msg.areYouSureYouWantToMakeSettingsChanges()}

    -

    {msg.defaultSettingsWillBeApplied()}

    - +

    {msg.changesWillBeMadeToFollowingVm()}

    +
    +
      + {vms.map(vm =>
    • {vm.get('name')}
    • )} +
    +
    +

    {msg.pressYesToConfirm()}

    } - confirm={{ title: msg.confirmChanges(), onClick: () => onSave(true, this.state.checkedVms) }} + confirm={{ title: msg.confirmChanges(), onClick: onConfirm }} onClose={onClose} /> ) } } SaveConfirmationModal.propTypes = { + vms: PropTypes.object.isRequired, show: PropTypes.bool, onClose: PropTypes.func.isRequired, - onSave: PropTypes.func.isRequired, + onConfirm: PropTypes.func.isRequired, } export default SaveConfirmationModal diff --git a/src/components/UserSettings/SettingsToolbar.js b/src/components/UserSettings/SettingsToolbar.js deleted file mode 100644 index a50b518000..0000000000 --- a/src/components/UserSettings/SettingsToolbar.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom' -import PropTypes from 'prop-types' -import { Toolbar } from 'patternfly-react' -import { msg } from '_/intl' - -import style from './style.css' - -const SettingsToolbar = ({ onSave, onCancel, onReset }) => { - const body = - - - - - - - if (document.getElementById('settings-toolbar')) { - return ReactDOM.createPortal( - body, - document.getElementById('settings-toolbar') - ) - } - return null -} - -SettingsToolbar.propTypes = { - onSave: PropTypes.func.isRequired, - onCancel: PropTypes.func.isRequired, - onReset: PropTypes.func.isRequired, -} - -export default SettingsToolbar diff --git a/src/components/UserSettings/VmSettings.js b/src/components/UserSettings/VmSettings.js index 999adb28b4..65fe1e6ad4 100644 --- a/src/components/UserSettings/VmSettings.js +++ b/src/components/UserSettings/VmSettings.js @@ -4,15 +4,13 @@ import Immutable from 'immutable' import { connect } from 'react-redux' import { push } from 'connected-react-router' -import { saveVmOptions, resetVmSettings } from '_/actions' -import { generateUnique } from '_/helpers' +import { saveVmsOptions, getByPage } from '_/actions' import { msg } from '_/intl' -import { Checkbox, Switch } from 'patternfly-react' -import NavigationPrompt from 'react-router-navigation-prompt' -import NavigationConfirmationModal from '../NavigationConfirmationModal' +import naturalCompare from 'string-natural-compare' +import InfiniteScroll from 'react-infinite-scroller' +import { Checkbox, Switch, Card, CardHeading, CardTitle } from 'patternfly-react' import Settings from '../Settings' -import SettingsToolbar from './SettingsToolbar' -import CounterAlert from './CounterAlert' +import SaveConfirmationModal from './SaveConfirmationModal' import style from './style.css' const EMPTY_MAP = Immutable.fromJS({}) @@ -21,20 +19,21 @@ const valuesMapper = { 'dontDisturb': (value) => value, 'autoConnect': (e) => e.target.checked, 'ctrlAltDel': (value) => value, - 'smartcard': (value) => value, + 'smartcard': (e) => e.target.checked, + 'fullScreenMode': (value) => value, 'displayUnsavedWarnings': (e) => e.target.checked, 'confirmForceShutdown': (e) => e.target.checked, 'confirmVmDeleting': (e) => e.target.checked, 'confirmVmSuspending': (e) => e.target.checked, - 'notifications': (value) => value, + 'disturb': (value) => !value, } class VmSettings extends Component { constructor (props) { super(props) - console.log(props.options.toJS()) + this.isMultiSelected = props.selectedVms.length !== 1 const globalSettings = props.options.get('options', EMPTY_MAP) - const vmSettings = props.options.getIn(['vms', props.vm.get('id')], EMPTY_MAP) + const vmSettings = !this.isMultiSelected ? props.options.getIn(['vms', props.selectedVms[0]], EMPTY_MAP) : EMPTY_MAP this.state = { values: { displayUnsavedWarnings: vmSettings.get('displayUnsavedWarnings', globalSettings.get('displayUnsavedWarnings', true)), @@ -45,112 +44,88 @@ class VmSettings extends Component { ctrlAltDel: vmSettings.get('ctrlAltDel', globalSettings.get('ctrlAltDel', false)), smartcard: vmSettings.get('smartcard', globalSettings.get('smartcard', false)), fullScreenMode: vmSettings.get('fullScreenMode', globalSettings.get('fullScreenMode', false)), - notifications: vmSettings.get('notifications', props.options.getIn(['options', 'allVmsNotifications'], false)), + disturb: !vmSettings.get('disturb', false), }, - saved: false, - errors: null, - changed: false, - updateValues: false, - changedFields: new Set(), - correlationId: null, + selectedVms: props.selectedVms, + showSaveConfirmation: false, } - this.handleChange = this.handleChange.bind(this) this.handleSave = this.handleSave.bind(this) + this.handleChange = this.handleChange.bind(this) this.handleCancel = this.handleCancel.bind(this) - this.handleReset = this.handleReset.bind(this) - this.handleSaveNotificationDissmised = this.handleSaveNotificationDissmised.bind(this) - this.handleErrorNotificationDissmised = this.handleErrorNotificationDissmised.bind(this) + this.buildSections = this.buildSections.bind(this) + this.handleVmCheck = this.handleVmCheck.bind(this) + this.handleAllVmsCheck = this.handleAllVmsCheck.bind(this) + this.handleSaveConfirmation = this.handleSaveConfirmation.bind(this) } - handleSave () { + handleSave (values, correlationId) { const { saveOptions } = this.props - const { values, changedFields } = this.state - const saveFields = [...changedFields].reduce((acc, cur) => ({ ...acc, [cur]: values[cur] }), {}) - this.setState({ correlationId: generateUnique('VmSettings-save_') }, () => saveOptions(saveFields, this.state.correlationId)) + const { selectedVms } = this.state + this.setState({ showSaveConfirmation: false }, () => saveOptions(values, selectedVms, correlationId)) } - static getDerivedStateFromProps (props, state) { - if (state.updateValues) { - const globalSettings = props.options.get('options', EMPTY_MAP) - const vmSettings = props.options.getIn(['vms', props.vm.get('id')], EMPTY_MAP) - return { - values: { - displayUnsavedWarnings: vmSettings.get('displayUnsavedWarnings', globalSettings.get('displayUnsavedWarnings', true)), - confirmForceShutdown: vmSettings.get('confirmForceShutdown', globalSettings.get('confirmForceShutdown', true)), - confirmVmDeleting: vmSettings.get('confirmVmDeleting', globalSettings.get('confirmVmDeleting', true)), - confirmVmSuspending: vmSettings.get('confirmVmSuspending', globalSettings.get('confirmVmSuspending', true)), - autoConnect: vmSettings.get('autoConnect', globalSettings.get('autoConnect', false)), - ctrlAltDel: vmSettings.get('ctrlAltDel', globalSettings.get('ctrlAltDel', false)), - smartcard: vmSettings.get('smartcard', globalSettings.get('smartcard', false)), - fullScreenMode: vmSettings.get('fullScreenMode', globalSettings.get('fullScreenMode', false)), - notifications: vmSettings.get('notifications', props.options.getIn(['options', 'allVmsNotifications'], false)), - }, - updateValues: false, - } - } - const res = props.options.getIn(['results', state.correlationId]) - if (state.correlationId !== null && res) { - if (res.status === 'OK') { - return { - correlationId: null, - saved: true, - changed: false, + handleCancel () { + this.props.goToVmPage() + } + + handleVmCheck (vmId) { + return () => { + this.setState(state => { + let selectedVms = new Set(state.selectedVms) + if (selectedVms.has(vmId)) { + selectedVms.delete(vmId) + } else { + selectedVms.add(vmId) } - } - if (res.status === 'ERROR' && res.details) { return { - corellationId: null, - errors: res.details, + selectedVms: [...selectedVms], } - } + }) } - return null } - handleChange (field, params) { - return (value) => { - const v = typeof value === 'object' ? Object.assign({}, value) : value - this.setState((state) => { - const { values, changedFields } = this.state - const changedFieldsClone = changedFields.add(field) - values[field] = valuesMapper[field](v, values[field], params) - return { values, changed: true, saved: false, changedFields: changedFieldsClone } + handleAllVmsCheck (e) { + if (e.target.checked) { + this.setState({ + selectedVms: this.props.vms.get('vms').keySeq().toJS(), + }) + } else { + this.setState({ + selectedVms: [], }) } } - handleSaveNotificationDissmised () { - this.setState({ saved: false }) - } - - handleErrorNotificationDissmised () { - this.setState({ errors: null }) - } - - handleCancel () { - this.props.goToVmPage() + handleSaveConfirmation (values, correlationId) { + if (this.isMultiSelected) { + this.saveValues = values + this.correlationId = correlationId + this.setState({ + showSaveConfirmation: true, + }) + } else { + this.handleSave(values, correlationId) + } } - handleReset () { - this.setState({ saved: true, changed: false, updateValues: true }) - this.props.resetSettings() + handleChange (values) { + this.setState({ values }) } - render () { - const { vm } = this.props - const { values } = this.state + buildSections (onChange) { + const { vms } = this.props + const { values, selectedVms } = this.state const idPrefix = 'vm-user-settings' - const sections = { + return { vm: { title: msg.virtualMachine(), - tooltip: msg.settingsWillBeAppliedToVm({ name: vm.get('name') }), fields: [ { title: msg.displayUnsavedChangesWarnings(), body: {msg.displayUnsavedChangesWarningsDetail()} , @@ -160,7 +135,7 @@ class VmSettings extends Component { body: {msg.confirmForceShutdownsDetails()} , @@ -170,7 +145,7 @@ class VmSettings extends Component { body: {msg.confirmDeletingVmDetails()} , @@ -180,16 +155,15 @@ class VmSettings extends Component { body: {msg.confirmSuspendingVmDetails()} , }, ], }, - console: vm.get('canUserUseConsole') && { + console: vms.get('vms').filter(vm => selectedVms.includes(vm.get('id')) && vm.get('canUserUseConsole')).size > 0 && { title: msg.console(), - tooltip: msg.settingsWillBeAppliedToVm({ name: vm.get('name') }), fields: [ { title: msg.fullScreenMode(), @@ -198,7 +172,7 @@ class VmSettings extends Component { bsSize='normal' title='normal' value={values.fullScreenMode} - onChange={(e, state) => this.handleChange('fullScreenMode')(state)} + onChange={(e, state) => onChange('fullScreenMode')(state)} />, }, { @@ -209,7 +183,7 @@ class VmSettings extends Component { bsSize='normal' title='normal' value={values.ctrlAltDel} - onChange={(e, state) => this.handleChange('ctrlAltDel')(state)} + onChange={(e, state) => onChange('ctrlAltDel')(state)} />, }, { @@ -217,7 +191,7 @@ class VmSettings extends Component { body: {msg.automaticConsoleConnectionDetails()} , @@ -228,7 +202,7 @@ class VmSettings extends Component { body: {msg.smartcardDetails()} , @@ -237,7 +211,7 @@ class VmSettings extends Component { }, notifications: { title: msg.notifications(), - tooltip: msg.settingsWillBeAppliedToVm({ name: vm.get('name') }), + tooltip: msg.notificationSettingsAffectAllMetricsNotifications(), fields: [ { title: msg.disableAllNotifications(), @@ -245,59 +219,110 @@ class VmSettings extends Component { id={`${idPrefix}-disable-notifications`} bsSize='normal' title='normal' - value={values.notifications} - onChange={(e, state) => this.handleChange('notifications')(state)} + value={values.disturb} + onChange={(e, state) => onChange('disturb')(state)} />, }, ], }, } + } + + render () { + const { vms, loadAnotherPage } = this.props + const { selectedVms } = this.state + const loadMore = () => { + if (vms.get('notAllPagesLoaded')) { + loadAnotherPage(vms.get('page') + 1) + } + } return (
    - - {({ isActive, onConfirm, onCancel }) => ( - - )} - - { this.state.saved &&
    - -
    } - { this.state.errors &&
    - msg[e]()).join(', '), - }) - } - onDismiss={this.handleErrorNotificationDissmised} +
    + -
    } - - + { this.isMultiSelected && + + + + {msg.selectedVirtualMachines()} + + +
    + + {msg.selectAllVirtualMachines()} + +
    +
    + + {vms.get('vms') + .toList() + .sort((vmA, vmB) => naturalCompare.caseInsensitive(vmA.get('name'), vmB.get('name'))) + .sort((vmA, vmB) => selectedVms.includes(vmA.get('id')) && !selectedVms.includes(vmB.get('id')) ? -1 : 0) + .map(vm =>
    + + {vm.get('name')} + +
    )} +
    +
    +
    + } + { this.isMultiSelected && + selectedVms.includes(vm.get('id'))).toList()} + show={this.state.showSaveConfirmation} + onConfirm={() => this.handleSave(this.saveValues, this.correlationId)} + onClose={() => this.setState({ showSaveConfirmation: false })} + /> + } +
    ) } } VmSettings.propTypes = { - vm: PropTypes.object.isRequired, + vms: PropTypes.object.isRequired, + selectedVms: PropTypes.array.isRequired, options: PropTypes.object.isRequired, saveOptions: PropTypes.func.isRequired, - resetSettings: PropTypes.func.isRequired, goToVmPage: PropTypes.func.isRequired, + loadAnotherPage: PropTypes.func.isRequired, } export default connect( (state) => ({ config: state.config, options: state.options, + vms: state.vms, }), - (dispatch, { vm }) => ({ - saveOptions: (values, correlationId) => dispatch(saveVmOptions({ values, vmId: vm.get('id') }, { correlationId })), - goToVmPage: () => dispatch(push(`/vm/${vm.get('id')}`)), - resetSettings: () => dispatch(resetVmSettings({ vmId: vm.get('id') })), + (dispatch, { selectedVms }) => ({ + saveOptions: (values, vmIds, correlationId) => dispatch(saveVmsOptions({ values, vmIds }, { correlationId })), + goToVmPage: () => { + if (selectedVms.length === 1) { + dispatch(push(`/vm/${selectedVms[0]}`)) + } else { + dispatch(push('/')) + } + }, + loadAnotherPage: (page) => dispatch(getByPage({ page })), }) )(VmSettings) diff --git a/src/components/UserSettings/VmsNotificationsList.js b/src/components/UserSettings/VmsNotificationsList.js deleted file mode 100644 index 50e1af21ac..0000000000 --- a/src/components/UserSettings/VmsNotificationsList.js +++ /dev/null @@ -1,142 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -import { connect } from 'react-redux' -import { getByPage } from '_/actions' -import { Checkbox, FormControl } from 'patternfly-react' -import InfiniteScroll from 'react-infinite-scroller' -import naturalCompare from 'string-natural-compare' -import { msg } from '_/intl' -import style from './style.css' - -function createCheckList ({ vmsNotifications, vms, defaultValue = false }) { - return vms.get('vms').map((v, k) => vmsNotifications[k] || defaultValue) -} - -class VmsNotificationsList extends React.Component { - constructor (props) { - super(props) - const allChecked = createCheckList(props).reduce((acc, curr) => acc && curr, props.defaultValue) - this.state = { - allChecked, - isOverflow: false, - filterValue: '', - } - this.ref = React.createRef() - this.handleCheckAll = this.handleCheckAll.bind(this) - this.handleCheck = this.handleCheck.bind(this) - this.updateOverflow = this.updateOverflow.bind(this) - this.handleFilter = this.handleFilter.bind(this) - } - handleCheckAll (e) { - let vmsNotifications = createCheckList(this.props).map(() => e.target.checked).toJS() - this.props.handleChange('vmsNotifications')(vmsNotifications) - this.props.handleChange('allVmsNotifications')(e.target.checked) - } - - updateOverflow () { - const state = { isOverflow: false } - if (this.ref.current.offsetHeight < this.ref.current.scrollHeight) { - state.isOverflow = true - } - if (this.state.isOverflow !== state.isOverflow) { - this.setState(state) - } - } - - handleCheck (vmId) { - const { handleChange } = this.props - return ({ target: { checked } }) => { - handleChange('vmsNotifications', { vmId })(checked) - if (!checked) { - handleChange('allVmsNotifications')(false) - } - } - } - - handleFilter ({ target: { value } }) { - this.setState({ filterValue: value }) - } - - static getDerivedStateFromProps (props, state) { - const allChecked = createCheckList(props).reduce((acc, curr) => acc && curr, true) - return { allChecked } - } - - componentDidUpdate (prevProps, prevState) { - if (prevState.allChecked !== this.state.allChecked) { - this.props.handleChange('allVmsNotifications')(this.state.allChecked, false) - } - this.updateOverflow() - } - - render () { - const { vmsNotifications, vms, handleChange, loadAnotherPage } = this.props - const loadMore = () => { - if (vms.get('notAllPagesLoaded')) { - loadAnotherPage(vms.get('page') + 1) - } - } - - let list = null - const sortedVms = vms.get('vms') - .sort((vmA, vmB) => naturalCompare.caseInsensitive(vmA.get('name'), vmB.get('name'))) - .filter(vm => vm.get('name').startsWith(this.state.filterValue)) - - const items = sortedVms.map(vm => { - return ( - - {vm.get('name')} - - ) - }) // ImmutableJS OrderedMap - - list = ( -
    - {[ - - {msg.allVirtualMachines()} - , - ...items.toArray(), - ]} -
    - ) - - return ( - - {(this.state.isOverflow || this.state.filterValue) && -
    - -
    - } -
    - - {list || (
    )} - -
    - - ) - } -} - -VmsNotificationsList.propTypes = { - vmsNotifications: PropTypes.object.isRequired, - vms: PropTypes.object.isRequired, - defaultValue: PropTypes.bool, - - handleChange: PropTypes.func.isRequired, - loadAnotherPage: PropTypes.func.isRequired, -} - -export default connect( - (state) => ({ - vms: state.vms, - }), - (dispatch) => ({ - loadAnotherPage: (page) => dispatch(getByPage({ page })), - }) -)(VmsNotificationsList) diff --git a/src/components/UserSettings/VmsSettingsList.js b/src/components/UserSettings/VmsSettingsList.js deleted file mode 100644 index d67e5ca4c1..0000000000 --- a/src/components/UserSettings/VmsSettingsList.js +++ /dev/null @@ -1,133 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -import { connect } from 'react-redux' -import { getByPage } from '_/actions' -import { Checkbox, FormControl } from 'patternfly-react' -import InfiniteScroll from 'react-infinite-scroller' -import naturalCompare from 'string-natural-compare' -import style from './style.css' - -class VmsSettingsList extends React.Component { - constructor (props) { - super(props) - this.state = { - vmsSaveSettings: {}, - isOverflow: false, - filterValue: '', - } - this.ref = React.createRef() - this.handleChange = this.handleChange.bind(this) - this.handleFilter = this.handleFilter.bind(this) - } - - updateOverflow () { - const state = { isOverflow: false } - if (this.ref.current.offsetHeight < this.ref.current.scrollHeight) { - state.isOverflow = true - } - if (this.state.isOverflow !== state.isOverflow) { - this.setState(state) - } - } - - handleChange (vmId) { - const globalOptions = this.props.options.get('options') - const vmsOptions = this.props.options.get('vms') - return ({ target: { checked } }) => { - const hasCustomSettings = !!(globalOptions.getIn(['vmsNotifications', vmId]) || vmsOptions.get(vmId) !== undefined) - console.log(hasCustomSettings, checked) - if (hasCustomSettings === checked) { - this.setState( - state => ({ vmsSaveSettings: { ...state.vmsSaveSettings, [vmId]: checked } }), - () => this.props.onChange(this.state.vmsSaveSettings) - ) - } else { - if (hasCustomSettings === checked && this.state.vmsSaveSettings[vmId]) { - let vmsSaveSettings = { ...this.state.vmsSaveSettings } - delete vmsSaveSettings[vmId] - this.setState( - { vmsSaveSettings }, - () => this.props.onChange(this.state.vmsSaveSettings) - ) - } - } - } - } - - componentDidUpdate () { - this.updateOverflow() - } - - handleFilter ({ target: { value } }) { - this.setState({ filterValue: value }) - } - - render () { - const { vms, loadAnotherPage, options } = this.props - const globalOptions = options.get('options') - const vmsOptions = options.get('vms') - const loadMore = () => { - if (vms.get('notAllPagesLoaded')) { - loadAnotherPage(vms.get('page') + 1) - } - } - - let list = null - const items = vms - .get('vms') - .sort((vmA, vmB) => naturalCompare.caseInsensitive(vmA.get('name'), vmB.get('name'))) - .filter(vm => vm.get('name').startsWith(this.state.filterValue)) - .map(vm => { - const vmId = vm.get('id') - const hasVmCustomSettings = globalOptions.getIn(['vmsNotifications', vmId]) || vmsOptions.get(vmId) - const vmChecked = this.state.vmsSaveSettings[vmId] !== undefined ? this.state.vmsSaveSettings[vmId] : !hasVmCustomSettings - return - {vm.get('name')} {!vmChecked && (you have manual settings)} - - }) - - list = ( -
    - {items.toArray()} -
    - ) - - return ( - - {(this.state.isOverflow || this.state.filterValue) && -
    - -
    - } -
    - - {list || (
    )} - -
    - - ) - } -} - -VmsSettingsList.propTypes = { - vms: PropTypes.object.isRequired, - options: PropTypes.object.isRequired, - - onChange: PropTypes.func.isRequired, - loadAnotherPage: PropTypes.func.isRequired, -} - -export default connect( - (state) => ({ - vms: state.vms, - options: state.options, - }), - (dispatch) => ({ - loadAnotherPage: (page) => dispatch(getByPage({ page })), - }) -)(VmsSettingsList) diff --git a/src/components/UserSettings/style.css b/src/components/UserSettings/style.css index a034998fbc..c6919893b8 100644 --- a/src/components/UserSettings/style.css +++ b/src/components/UserSettings/style.css @@ -3,27 +3,18 @@ overflow: auto; } -.alert-container { - position: absolute; - top: 5%; - left: 25%; - width: 50%; -} - -.toolbar :global(.toolbar-pf.row) { - padding-bottom: 10px; - margin-left: 0; +.filter-vms-box { + margin-right: 20px; + margin-bottom: 10px; } -.toolbar :global(.toolbar-pf-action-right) button { - margin-right: 10px; +.vms-settings-box { + display: flex; } -:global(#settings-toolbar) { - margin-left: -20px; +.vms-card { + margin-left: 20px; + margin-top: 20px; + flex-grow: 1; + overflow: hidden; } - -.filter-vms-box { - margin-right: 20px; - margin-bottom: 10px; -} \ No newline at end of file diff --git a/src/components/VmConsole/VmConsoleInstructionsModal.js b/src/components/VmConsole/VmConsoleInstructionsModal.js index d7eb3aa63f..94cfc18898 100644 --- a/src/components/VmConsole/VmConsoleInstructionsModal.js +++ b/src/components/VmConsole/VmConsoleInstructionsModal.js @@ -1,6 +1,7 @@ import React from 'react' import { Icon, Modal, Button } from 'patternfly-react' import style from './style.css' +import sharedStyle from '_/components/sharedStyle.css' import { msg } from '../../intl' import PropTypes from 'prop-types' @@ -23,7 +24,7 @@ class VmConsoleInstructionsModal extends React.Component { render () { const { disabled } = this.props return
    - diff --git a/src/components/VmConsole/style.css b/src/components/VmConsole/style.css index abf39fc514..70815de9b8 100644 --- a/src/components/VmConsole/style.css +++ b/src/components/VmConsole/style.css @@ -10,10 +10,6 @@ display: flex; } -:global(.toolbar-pf .form-group) .color-blue { - color: #0088ce; -} - .console-detail-container dt { clear: left; float: left; diff --git a/src/components/VmsList/Vm.js b/src/components/VmsList/Vm.js index 83182a770d..3ca790778a 100644 --- a/src/components/VmsList/Vm.js +++ b/src/components/VmsList/Vm.js @@ -3,9 +3,11 @@ import PropTypes from 'prop-types' import { connect } from 'react-redux' import { withRouter } from 'react-router-dom' +import { Checkbox } from 'patternfly-react' import BaseCard from './BaseCard' import sharedStyle from '../sharedStyle.css' +import style from './style.css' import VmActions from '../VmActions' import VmStatusIcon from '../VmStatusIcon' @@ -18,7 +20,7 @@ import { enumMsg } from '_/intl' /** * Single icon-card in the list for a VM */ -const Vm = ({ vm, icons, os, onStart }) => { +const Vm = ({ vm, checked, icons, os, onStart, onCheck }) => { const idPrefix = `vm-${vm.get('name')}` const state = vm.get('status') const stateValue = enumMsg('VmStatus', state) @@ -28,6 +30,7 @@ const Vm = ({ vm, icons, os, onStart }) => { return ( + {osName} @@ -43,7 +46,9 @@ Vm.propTypes = { vm: PropTypes.object.isRequired, icons: PropTypes.object.isRequired, os: PropTypes.object.isRequired, + checked: PropTypes.bool, onStart: PropTypes.func.isRequired, + onCheck: PropTypes.func.isRequired, } export default withRouter(connect( diff --git a/src/components/VmsList/Vms.js b/src/components/VmsList/Vms.js index c0202c0bc7..bda859b23a 100644 --- a/src/components/VmsList/Vms.js +++ b/src/components/VmsList/Vms.js @@ -1,17 +1,58 @@ import React from 'react' +import ReactDOM from 'react-dom' import PropTypes from 'prop-types' +import { Link } from 'react-router-dom' +import { Icon, Button } from 'patternfly-react' import { connect } from 'react-redux' import style from './style.css' +import sharedStyle from '_/components/sharedStyle.css' import Vm from './Vm' import Pool from './Pool' import ScrollPositionHistory from '../ScrollPositionHistory' import { getByPage } from '_/actions' import { filterVms, sortFunction } from '_/utils' +import { msg } from '_/intl' import InfiniteScroll from 'react-infinite-scroller' import Loader, { SIZES } from '../Loader' +const VmsSettingsButton = ({ checkedVms = [] }) => { + const container = document.getElementById('vm-settings-btn-box') + if (container) { + return ReactDOM.createPortal( + + + {msg.vmSettings()} + , + container + ) + } + return null +} + +VmsSettingsButton.propTypes = { + checkedVms: PropTypes.array, +} + +const SelectAllVmsButton = ({ onClick }) => { + const container = document.getElementById('select-all-vms-btn-box') + if (container) { + return ReactDOM.createPortal( + , + container + ) + } + return null +} + +SelectAllVmsButton.propTypes = { + onClick: PropTypes.func.isRequired, +} + /** * Use Patternfly 'Single Select Card View' pattern to show every VM and Pool * available to the current user. @@ -19,7 +60,10 @@ import Loader, { SIZES } from '../Loader' class Vms extends React.Component { constructor (props) { super(props) + this.state = { checkedVms: new Set() } this.loadMore = this.loadMore.bind(this) + this.checkVm = this.checkVm.bind(this) + this.selectAll = this.selectAll.bind(this) } loadMore () { @@ -28,6 +72,25 @@ class Vms extends React.Component { } } + checkVm (vmId) { + this.setState((state) => { + const checkedVms = new Set(state.checkedVms) + if (!checkedVms.has(vmId)) { + checkedVms.add(vmId) + } else { + checkedVms.delete(vmId) + } + return { checkedVms } + }) + } + + selectAll () { + const { vms } = this.props + this.setState({ + checkedVms: new Set(vms.get('vms').keySeq().toJS()), + }) + } + render () { const { vms, alwaysShowPoolCard } = this.props @@ -37,7 +100,17 @@ class Vms extends React.Component { const sortedVms = vms.get('vms').filter(vm => filterVms(vm, filters)).toList().sort(sortFunction(sort)) const sortedPools = vms.get('pools') - .filter(pool => alwaysShowPoolCard || (pool.get('vmsCount') < pool.get('maxUserVms') && pool.get('size') > 0 && filterVms(pool, filters))) + .filter(pool => + pool.get('vm') && + ( + alwaysShowPoolCard || + ( + pool.get('vmsCount') < pool.get('maxUserVms') && + pool.get('size') > 0 && + filterVms(pool, filters) + ) + ) + ) .toList() .sort(sortFunction(sort)) // TODO: sort vms and pools together! @@ -49,11 +122,20 @@ class Vms extends React.Component { loader={} useWindow={false} > + +
    - {sortedVms.map(vm => )} + {sortedVms.map(vm => + this.checkVm(vm.get('id'))} + /> + )} {sortedPools.map(pool => )}
    diff --git a/src/components/VmsList/style.css b/src/components/VmsList/style.css index 7fc9f83db3..bcc2331be9 100644 --- a/src/components/VmsList/style.css +++ b/src/components/VmsList/style.css @@ -57,3 +57,9 @@ .status-height { margin-top: 5px !important; } + +.vm-checkbox { + margin-left: 30px; + position: absolute; + margin-top: 20px; +} diff --git a/src/components/VmsPageHeader/UserMenu.js b/src/components/VmsPageHeader/UserMenu.js index abf500211a..7448f1c69e 100644 --- a/src/components/VmsPageHeader/UserMenu.js +++ b/src/components/VmsPageHeader/UserMenu.js @@ -1,7 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' -import { Link } from 'react-router-dom' import { logout } from '_/actions' @@ -19,9 +18,6 @@ const UserMenu = ({ config, onLogout }) => {
      -
    • - Settings -
    • diff --git a/src/components/VmsPageHeader/index.js b/src/components/VmsPageHeader/index.js index 65fba45ad0..df7274d0e0 100644 --- a/src/components/VmsPageHeader/index.js +++ b/src/components/VmsPageHeader/index.js @@ -2,6 +2,8 @@ import React from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' +import { Link } from 'react-router-dom' +import { Icon } from 'patternfly-react' import VmUserMessages from '../VmUserMessages' import UserMenu from './UserMenu' @@ -33,6 +35,16 @@ const VmsPageHeader = ({ page, onRefresh }) => { +
    • + + + + + +
    diff --git a/src/components/sharedStyle.css b/src/components/sharedStyle.css index b3d5b8140b..7a67b802fc 100644 --- a/src/components/sharedStyle.css +++ b/src/components/sharedStyle.css @@ -93,3 +93,11 @@ 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } + +:global(.toolbar-pf .form-group) .color-blue { + color: #0088ce; +} + +.settings-icon span { + margin-right: 5px; +} diff --git a/src/constants/index.js b/src/constants/index.js index 884eec346e..379ee15a83 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -68,16 +68,12 @@ export const REMOVE_SNAPSHOT_REMOVAL_PENDING_TASK = 'REMOVE_SNAPSHOT_REMOVAL_PEN export const REMOVE_SNAPSHOT_RESTORE_PENDING_TASK = 'REMOVE_SNAPSHOT_RESTORE_PENDING_TASK' export const REMOVE_VM = 'REMOVE_VM' export const REMOVE_VMS = 'REMOVE_VMS' -export const RESET_GLOBAL_SETTINGS = 'RESET_GLOBAL_SETTINGS' -export const RESET_OPTIONS = 'RESET_OPTIONS' -export const RESET_VM_SETTINGS = 'RESET_VM_SETTINGS' export const RESTART_VM = 'RESTART_VM' export const SAVE_FILTERS = 'SAVE_FILTERS' export const SAVE_GLOBAL_OPTIONS = 'SAVE_GLOBAL_OPTIONS' export const SAVE_OPTION = 'SAVE_OPTION' -export const SAVE_OPTION_TO_VMS = 'SAVE_OPTION_TO_VMS' export const SAVE_SSH_KEY = 'SAVE_SSH_KEY' -export const SAVE_VM_OPTIONS = 'SAVE_VM_OPTIONS' +export const SAVE_VMS_OPTIONS = 'SAVE_VMS_OPTIONS' export const SELECT_POOL_DETAIL = 'SELECT_POOL_DETAIL' export const SELECT_VM_DETAIL = 'SELECT_VM_DETAIL' export const SET_ADMINISTRATOR = 'SET_ADMINISTRATOR' diff --git a/src/index-nomodules.css b/src/index-nomodules.css index 94d156e4ae..fad13b7c74 100644 --- a/src/index-nomodules.css +++ b/src/index-nomodules.css @@ -226,3 +226,8 @@ a.disabled { textarea { resize: vertical; } + +.toolbar-pf-action-right > div { + display: inline-block; + margin-left: 5px; +} diff --git a/src/intl/index.js b/src/intl/index.js index fc0301bfe4..0f6bb0f93b 100644 --- a/src/intl/index.js +++ b/src/intl/index.js @@ -29,7 +29,7 @@ const options = JSON.parse(loadFromLocalStorage('options')) || {} /** * Currently selected locale */ -export const locale: string = getLocaleFromUrl() || options.options.language || getBrowserLocale() || DEFAULT_LOCALE +export const locale: string = getLocaleFromUrl() || (options.options && options.options.language) || getBrowserLocale() || DEFAULT_LOCALE function getBrowserLocale (): ?string { if (window.navigator.language) { diff --git a/src/intl/messages.js b/src/intl/messages.js index 3080ec2c7f..6cb78aae3a 100644 --- a/src/intl/messages.js +++ b/src/intl/messages.js @@ -37,7 +37,7 @@ export const messages: { [messageId: string]: MessageType } = { areYouSureYouWantToDeleteDisk: 'Are you sure you want to delete disk {diskName}?', areYouSureYouWantToDeleteNic: 'Are you sure you want to delete NIC {nicName}?', areYouSureYouWantToDeleteSnapshot: 'Are you sure you want to delete snapshot {snapshotName}?', - areYouSureYouWantToMakeSettingsChanges: 'Are you sure you want to make changes to your account settings?', + areYouSureYouWantToMakeSettingsChanges: 'Are you sure you want to make changes to your VM settings?', areYouSureYouWantToResetSettings: 'Are you sure you want to reset your account settings?', areYouSureYouWantToRestoreSnapshot: 'Are you sure you want to restore snapshot {snapshotName}?', authorizationExpired: 'Authorization expired. The page is going to be reloaded to re-login.', @@ -59,6 +59,7 @@ export const messages: { [messageId: string]: MessageType } = { cdromBoot: 'CD-ROM', changeCd: 'Change CD', changesWasSavedSuccesfully: 'Changes was saved succesfully!', + changesWillBeMadeToFollowingVm: 'Settings changes will be made to the following VMs:', clear: 'Clear', clearAll: 'Clear all', clearAllFilters: 'Clear All Filters', @@ -94,6 +95,7 @@ export const messages: { [messageId: string]: MessageType } = { console: 'Console', consoleInstructions: 'Console Instructions', consoleInUseContinue: 'Console in use, continue?', + consoleSettings: 'Console Settings', containsConfigurationAndDisksWhichWillBeUsedToCreateThisVm: 'Contains the configuration and disks which will be used to create this virtual machine. Please customize as needed.', continueSessionSecondary: { message: 'To continue with your session, click on the \'Continue\' button.', @@ -120,7 +122,6 @@ export const messages: { [messageId: string]: MessageType } = { dataCenterChangesWithCluster: 'Data Center cannot be changed directly. It correlates with the Cluster.', daysShort: 'd', defaultButton: 'Default', - defaultSettingsWillBeApplied: 'These will be the default settings that will be applied to all VMs that do not have custom settings set. The following selected VMs will have their settings updated based off of the changes you have made to your account settings. Please, confirm these changes before saving.', definedMemory: 'Defined Memory', delete: 'Delete', description: 'Description', @@ -419,6 +420,8 @@ export const messages: { [messageId: string]: MessageType } = { }, notEditableForPoolsOrPoolVms: 'Not editable for Pools or pool VMs.', notifications: 'Notifications', + notificationSettingsAffectAllNotifications: 'Notification settings applied here affect all notifications.', + notificationSettingsAffectAllMetricsNotifications: 'Notification settings applied here affect all of float-metrics’s notifications.', noVmAvailable: 'No VM available.', noVmAvailableForLoggedUser: 'No VM is available for the logged user.', numberOfMinutes: '{minute} minutes', @@ -440,6 +443,7 @@ export const messages: { [messageId: string]: MessageType } = { pleaseEnterValidDiskName: 'Please enter a valid disk name. Only lower-case and upper-case letters, numbers, and \'_\',\'-\',\'.\' are allowed.', pleaseEnterValidVmName: 'Please enter a valid virtual machine name. Only lower-case and upper-case letters, numbers, and \'_\',\'-\',\'.\' are allowed.', preserveDisks: 'Preserve disks', + pressYesToConfirm: 'Press \'Yes\' to confirm these changes.', publicSSHKey: 'Specify public key for access to guest serial console via SSH authentication.', rdpConsole: 'RDP Console', rdpConsoleOpen: 'Open RDP Console', @@ -455,6 +459,7 @@ export const messages: { [messageId: string]: MessageType } = { message: 'Refresh', description: 'Reload data from server', }, + refreshInterval: 'Refresh Interval', remoteViewerConnection: 'Remote Viewer Connection', remoteViewerAvailable: 'Remote Viewer is available for most operating systems. To install it, search for it in GNOME Software or run the following:', remove: 'Remove', @@ -481,11 +486,12 @@ export const messages: { [messageId: string]: MessageType } = { secondsShort: 's', sendShortcutKey: 'Send Key', sendCtrlAltDel: 'Ctrl+Alt+Del', + selectedVirtualMachines: 'Selected Virtual Machines', + selectAllVirtualMachines: 'Select all virtual machines', sessionExpired: { message: 'Your session is about to timeout due to inactivity.', description: 'Primary message for SessionTimeout modal component', }, - settingsWillBeAppliedToVm: 'This settings will be applied only to {name}.', shutdown: 'Shutdown', shutdownVm: 'Shutdown the VM', shutdownVmQuestion: 'Are you sure you want to Shutdown the VM?', @@ -593,6 +599,7 @@ export const messages: { [messageId: string]: MessageType } = { useBrowserBelow: 'Please use one of the browsers below.', useCtrlAltEnd: 'Use Ctrl+Alt+End', username: 'Username', + userSettings: 'User Settings', usingRemoteViewer: 'Using a remote viewer relies on a downloaded .vv file.', vcpuTopology: 'VCPU Topology', virtualMachine: 'Virtual Machine', @@ -601,6 +608,7 @@ export const messages: { [messageId: string]: MessageType } = { vmHasPendingConfigurationChanges: 'This VM has pending configurations changes that will be applied once the VM is shutdown (or rebooted).', vmMemory: 'VM Memory', vmPortal: 'VM Portal', + vmSettings: 'VM Settings', vmType_desktop: 'Desktop', vmType_highPerformance: 'High Performance', vmType_server: 'Server', diff --git a/src/ovirtapi/index.js b/src/ovirtapi/index.js index 39a7fb2fb7..ab81375df8 100644 --- a/src/ovirtapi/index.js +++ b/src/ovirtapi/index.js @@ -17,7 +17,6 @@ import { httpPost, httpPut, httpDelete, - httpHead, } from './transport' import * as Transforms from './transform' @@ -104,11 +103,6 @@ const OvirtApi = { } return httpGet({ url }) }, - isVmExist ({ vmId }: { vmId: string }): Promise { - assertLogin({ methodName: 'isVmExist' }) - let url = `${AppConfiguration.applicationContext}/api/vms/${vmId}` - return httpHead({ url }) - }, getVmsByPage ({ page, additional }: { page: number, additional: Array }): Promise { assertLogin({ methodName: 'getVmsByPage' }) let url = `${AppConfiguration.applicationContext}/api/vms/;max=${AppConfiguration.pageLimit}?search=SORTBY NAME ASC page ${page}` diff --git a/src/ovirtapi/transport.js b/src/ovirtapi/transport.js index d3c8a70eb0..88be8a7b1c 100644 --- a/src/ovirtapi/transport.js +++ b/src/ovirtapi/transport.js @@ -87,33 +87,6 @@ function httpGet ({ url, custHeaders = {} }: GetRequestType): Promise { }) } -function httpHead ({ url, custHeaders = {} }: GetRequestType): Promise { - const myCounter = getCounter++ - const requestId = notifyStart('HEAD', url) - const headers = { - 'Accept': 'application/json', - 'Authorization': `Bearer ${_getLoginToken()}`, - 'Accept-Language': AppConfiguration.queryParams.locale, // can be: undefined, empty or string - 'Filter': Selectors.getFilter(), - ...custHeaders, - } - - console.log(`http HEAD[${myCounter}] -> url: "${url}", headers: ${logHeaders(headers)}`) - return $.ajax(url, { - type: 'HEAD', - headers, - }) - .then((data: Object): Object => { - notifyStop(requestId) - return data - }) - .catch((data: Object): Promise => { - console.log(`Ajax HEAD failed: ${JSON.stringify(data)}`) - notifyStop(requestId) - return Promise.reject(data) - }) -} - function httpPost ({ url, input, contentType = 'application/json' }: InputRequestType): Promise { const requestId = notifyStart('POST', url) return $.ajax(url, { @@ -197,5 +170,4 @@ export { httpPost, httpPut, httpDelete, - httpHead, } diff --git a/src/reducers/options.js b/src/reducers/options.js index 52d458027b..7cd4e85fd7 100644 --- a/src/reducers/options.js +++ b/src/reducers/options.js @@ -3,7 +3,6 @@ import { SET_SSH_KEY, SET_OPTION, SET_OPTION_TO_VMS, - RESET_OPTIONS, SET_OPTIONS_SAVE_RESULTS, } from '_/constants' import { actionReducer } from './utils' @@ -49,19 +48,6 @@ const options = actionReducer(initialState, { } return options }, - [RESET_OPTIONS] (state, { payload: { vmId } }) { - if (!vmId) { - return state.set('options', Immutable.fromJS({ - ssh: { - key: null, - id: undefined, - }, - language: locale, - updateRate: AppConfiguration.schedulerFixedDelayInSeconds, - })) - } - return state.setIn(['vms', vmId], EMPTY_MAP) - }, [SET_SSH_KEY] (state, { payload: { key, id } }) { return state.setIn(['options', 'ssh'], { key: key || null, id }) }, diff --git a/src/reducers/vms.js b/src/reducers/vms.js index 2b02346711..203f980cda 100644 --- a/src/reducers/vms.js +++ b/src/reducers/vms.js @@ -209,7 +209,7 @@ const vms = actionReducer(initialState, { state.get('vms').toList().map(vm => { // Check if vm is in actual pool and its down, checking for down vms is for not count that vms in admin mode if ( - vm.getIn(['pool', 'id']) && + !!vm.getIn(['pool', 'id']) && ( vm.get('status') !== 'down' || state.getIn(['pools', vm.getIn(['pool', 'id']), 'type']) === 'manual' diff --git a/src/routes.js b/src/routes.js index a9c1f54ed3..b00ffc9c07 100644 --- a/src/routes.js +++ b/src/routes.js @@ -63,6 +63,15 @@ export default function getRoutes (vms) { closeable: true, type: DIALOG_PAGE_TYPE, }, + { + path: '/vms-settings/:id+', + title: 'VM Settings', + component: VmSettingsPage, + toolbars: () =>
    , + closeable: true, + isToolbarFullWidth: true, + type: VM_SETTINGS_PAGE_TYPE, + }, { path: '/vm/:id', @@ -81,7 +90,7 @@ export default function getRoutes (vms) { type: CONSOLE_PAGE_TYPE, }, { - path: '/vm/:id/settings', + path: '/vm/:id+/settings', title: (match) => 'settings' || match.params.id, component: VmSettingsPage, toolbars: () =>
    , @@ -102,7 +111,7 @@ export default function getRoutes (vms) { { path: '/settings', exact: true, - title: () => 'Settings', + title: () => 'Account Settings', component: GlobalSettingsPage, toolbars: () =>
    , closeable: true, diff --git a/src/sagas/index.js b/src/sagas/index.js index 9780849b5c..525c51992d 100644 --- a/src/sagas/index.js +++ b/src/sagas/index.js @@ -262,7 +262,10 @@ function* refreshPoolPage ({ id }) { } function* refreshVmSettingsPage ({ id }) { - yield selectVmDetail(actionSelectVmDetail({ vmId: id })) + const ids = id.split('/') + for (let vmId of ids) { + yield selectVmDetail(actionSelectVmDetail({ vmId })) + } } const pagesRefreshers = { diff --git a/src/sagas/options.js b/src/sagas/options.js index f2b31d5fa6..1acc77490c 100644 --- a/src/sagas/options.js +++ b/src/sagas/options.js @@ -2,42 +2,24 @@ import Api from '_/ovirtapi' import Selectors from '_/selectors' import { all, put, select, takeLatest, takeEvery } from 'redux-saga/effects' -import { saveSSHKey as saveSSHKeyAction, setOptionsSaveResults, resetOptions, setOption, setOptionToVms, getSSHKey, setSSHKey, resetVmSettings } from '_/actions' +import { saveSSHKey as saveSSHKeyAction, setOptionsSaveResults, setOption, setOptionToVms, getSSHKey, setSSHKey } from '_/actions' import { saveToLocalStorage } from '_/storage' import { callExternalAction } from './utils' import { GET_SSH_KEY, - RESET_GLOBAL_SETTINGS, - RESET_VM_SETTINGS, SAVE_GLOBAL_OPTIONS, SAVE_OPTION, - SAVE_OPTION_TO_VMS, SAVE_SSH_KEY, - SAVE_VM_OPTIONS, + SAVE_VMS_OPTIONS, } from '_/constants' function* saveOptionsToLocalStorage () { const options = yield select(state => state.options.delete('results').toJS()) - saveToLocalStorage(`options`, JSON.stringify(options)) -} - -function* checkVmNotifications () { - let vms = yield select(state => state.options.get('vms')) - const vmIds = Object.keys(vms.toJS()) - const vmsCheck = yield all( - vmIds.reduce( - (acc, vmId) => - ({ ...acc, [vmId]: callExternalAction('isVmExist', Api.isVmExist, { payload: { vmId } }, true) }), - {} - ) - ) - yield all(vms.filter((value, vmId) => vmsCheck[vmId] === undefined).map((vm, vmId) => put(resetVmSettings({ vmId })))) - yield saveOptionsToLocalStorage() + saveToLocalStorage('options', JSON.stringify(options)) } export function* refreshUserSettingsPage () { - yield checkVmNotifications() yield fetchSSHKey(getSSHKey({ userId: Selectors.getUserId() })) } @@ -63,79 +45,32 @@ function* saveOption (payload) { yield saveOptionsToLocalStorage() } -function* saveOptionToVms (payload) { - yield put(setOptionToVms(payload)) - yield saveOptionsToLocalStorage() -} - -export function* resetGlobalOptions () { - yield put(resetOptions()) - const options = yield select(state => state.options.toJS()) - saveToLocalStorage(`options`, JSON.stringify(options)) -} - -export function* resetVmOptions (actions) { - yield put(resetOptions(actions.payload)) +function* saveOptionToVms (value, { vmIds, key }) { + yield put(setOptionToVms({ key, value, vmIds })) yield saveOptionsToLocalStorage() } -function* saveGlobalOptionWithVmsOption (value, { checkedVms, key }) { - const prevValue = yield select(state => state.options.getIn(['options', key])) - yield saveOption({ key, value }) - if (checkedVms) { - const checkedVmIds = Object.keys(checkedVms).filter(k => checkedVms[k]) - yield saveOptionToVms({ key, value, vmIds: checkedVmIds }) - const uncheckedVmIds = Object.keys(checkedVms).filter(k => !checkedVms[k]) - yield saveOptionToVms({ key, value: prevValue, vmIds: uncheckedVmIds }) - } -} - -function* saveVmOption (value, { vmId, key }) { - yield saveOption({ key, value, vmId }) -} - const saveGlobalMapper = { 'language': function* (value) { yield saveOption({ key: 'language', value }) }, 'sshKey': function* (value, { sshId, userId }) { const res = yield saveSSHKey(saveSSHKeyAction({ sshId, key: value, userId })); return res }, 'dontDisturb': function* (value) { yield saveOption({ key: 'dontDisturb', value }) }, - 'vmsNotifications': function* (values) { - yield saveOptionToVms({ key: 'notifications', values }) - const allVmsNotifications = Object.values(values).reduce((acc, cur) => acc && cur, true) - if (!allVmsNotifications) { - yield saveOption({ key: 'allVmsNotifications', value: false }) - } - }, 'updateRate': function* (value) { yield saveOption({ key: 'updateRate', value }) }, 'dontDisturbFor': function* (value) { yield saveOption({ key: 'dontDisturbFor', value }) yield saveOption({ key: 'dontDisturbStart', value: Date.now() }) }, - 'allVmsNotifications': function* (value) { yield saveOption({ key: 'allVmsNotifications', value }) }, - 'displayUnsavedWarnings': saveGlobalOptionWithVmsOption, - 'confirmForceShutdown': saveGlobalOptionWithVmsOption, - 'confirmVmDeleting': saveGlobalOptionWithVmsOption, - 'confirmVmSuspending': saveGlobalOptionWithVmsOption, - 'fullScreenMode': saveGlobalOptionWithVmsOption, - 'ctrlAltDel': saveGlobalOptionWithVmsOption, - 'smartcard': saveGlobalOptionWithVmsOption, - 'autoConnect': saveGlobalOptionWithVmsOption, } const saveVmMapper = { - 'displayUnsavedWarnings': saveVmOption, - 'confirmForceShutdown': saveVmOption, - 'confirmVmDeleting': saveVmOption, - 'confirmVmSuspending': saveVmOption, - 'fullScreenMode': saveVmOption, - 'ctrlAltDel': saveVmOption, - 'smartcard': saveVmOption, - 'autoConnect': saveVmOption, - 'notifications': function* (value, { vmId }) { - yield saveOption({ key: 'notifications', value, vmId }) - if (!value) { - yield saveOption({ key: 'allVmsNotifications', value: false }) - } - }, + 'displayUnsavedWarnings': saveOptionToVms, + 'confirmForceShutdown': saveOptionToVms, + 'confirmVmDeleting': saveOptionToVms, + 'confirmVmSuspending': saveOptionToVms, + 'fullScreenMode': saveOptionToVms, + 'ctrlAltDel': saveOptionToVms, + 'smartcard': saveOptionToVms, + 'autoConnect': saveOptionToVms, + 'disturb': saveOptionToVms, } function* handleSavingResults (results, meta) { @@ -157,20 +92,20 @@ export function* saveGlobalOptions (action) { .reduce((acc, [key, value]) => ({ ...acc, - [key]: saveGlobalMapper[key](value, { sshId, userId, checkedVms: action.payload.checkedVms, key }), + [key]: saveGlobalMapper[key](value, { sshId, userId, key }), }), {}) ) yield handleSavingResults(res, action.meta) } -export function* saveVmOptions (action) { +export function* saveVmsOptions (action) { const res = yield all( Object.entries(action.payload.values) .reduce((acc, [key, value]) => ({ ...acc, - [key]: saveVmMapper[key](value, { vmId: action.payload.vmId, key }), + [key]: saveVmMapper[key](value, { vmIds: action.payload.vmIds, key }), }), {}) ) @@ -179,11 +114,8 @@ export function* saveVmOptions (action) { export default [ takeEvery(SAVE_OPTION, saveOption), - takeEvery(SAVE_OPTION_TO_VMS, saveOptionToVms), - takeEvery(RESET_GLOBAL_SETTINGS, resetGlobalOptions), - takeEvery(RESET_VM_SETTINGS, resetVmOptions), takeEvery(SAVE_SSH_KEY, saveSSHKey), takeLatest(SAVE_GLOBAL_OPTIONS, saveGlobalOptions), - takeLatest(SAVE_VM_OPTIONS, saveVmOptions), + takeLatest(SAVE_VMS_OPTIONS, saveVmsOptions), takeLatest(GET_SSH_KEY, fetchSSHKey), ] diff --git a/src/sagas/utils.js b/src/sagas/utils.js index d4515f6182..2f39bcd0cb 100644 --- a/src/sagas/utils.js +++ b/src/sagas/utils.js @@ -47,9 +47,9 @@ export function* callExternalAction (methodName, method, action = {}, canBeMissi const result = yield call(method, action.payload || {}) return result } catch (e) { - const isFiltered = yield select(state => state.options.getIn(['vms', action.payload.vmId, 'notifications'], false)) + const isVmDisturb = yield select(state => state.options.getIn(['vms', action.payload.vmId, 'disturb'], false)) const isDontDisturb = yield select(state => state.options.getIn(['options', 'dontDisturb'], false)) - if (!canBeMissing && !(isDontDisturb && isFiltered)) { + if (!canBeMissing && !(isDontDisturb && !isVmDisturb)) { console.log(`External action exception: ${JSON.stringify(e)}`) if (e.status === 401) { // Unauthorized