From 247505ac8cd4be20376b74839a78ad50e8de338b Mon Sep 17 00:00:00 2001 From: Bohdan Iakymets Date: Fri, 18 Oct 2019 14:23:56 +0200 Subject: [PATCH] Updated design of user settings --- src/actions/options.js | 51 +-- src/components/Pages/index.js | 19 +- .../CounterAlert.js | 0 src/components/Settings/SettingsBase.js | 83 +++++ src/components/Settings/SettingsToolbar.js | 64 ++++ src/components/Settings/index.js | 219 +++++------ src/components/Settings/style.css | 26 +- src/components/Toolbar/VmsListToolbar.js | 2 + src/components/Toolbar/index.js | 12 +- src/components/Toolbar/style.css | 5 + src/components/UserSettings/GlobalSettings.js | 342 ++++-------------- .../UserSettings/ResetConfirmationModal.js | 33 -- .../UserSettings/SaveConfirmationModal.js | 17 +- .../UserSettings/SettingsToolbar.js | 38 -- src/components/UserSettings/VmSettings.js | 278 +++++++------- .../UserSettings/VmsNotificationsList.js | 142 -------- .../UserSettings/VmsSettingsList.js | 133 ------- src/components/UserSettings/style.css | 29 +- .../VmConsole/VmConsoleInstructionsModal.js | 3 +- src/components/VmConsole/style.css | 4 - src/components/VmsList/Vm.js | 10 +- src/components/VmsList/Vms.js | 84 ++++- src/components/VmsList/style.css | 8 +- src/components/VmsPageHeader/UserMenu.js | 4 - src/components/VmsPageHeader/index.js | 12 + src/components/sharedStyle.css | 8 + src/constants/index.js | 6 +- src/index-nomodules.css | 5 + src/intl/index.js | 2 +- src/intl/messages.js | 16 +- src/intl/translated-messages.json | 16 - src/ovirtapi/index.js | 6 - src/ovirtapi/transport.js | 28 -- src/reducers/options.js | 14 - src/reducers/vms.js | 2 +- src/routes.js | 18 +- src/sagas/index.js | 5 +- src/sagas/options.js | 104 +----- src/sagas/utils.js | 4 +- 39 files changed, 721 insertions(+), 1131 deletions(-) rename src/components/{UserSettings => Settings}/CounterAlert.js (100%) create mode 100644 src/components/Settings/SettingsBase.js create mode 100644 src/components/Settings/SettingsToolbar.js delete mode 100644 src/components/UserSettings/ResetConfirmationModal.js delete mode 100644 src/components/UserSettings/SettingsToolbar.js delete mode 100644 src/components/UserSettings/VmsNotificationsList.js delete mode 100644 src/components/UserSettings/VmsSettingsList.js 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 354c018b2a..727aca9735 100644 --- a/src/components/Pages/index.js +++ b/src/components/Pages/index.js @@ -31,35 +31,36 @@ 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 } render () { - const { vms } = this.props - const { vmId } = this.state + const { vms, route } = this.props + 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 } } VmSettingsPage.propTypes = { vms: PropTypes.object.isRequired, + route: PropTypes.object.isRequired, match: RouterPropTypeShapes.match.isRequired, } const VmSettingsPageConnected = connect( 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..6eb9b3ee74 --- /dev/null +++ b/src/components/Settings/SettingsToolbar.js @@ -0,0 +1,64 @@ +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..e06bed347b 100644 --- a/src/components/Settings/index.js +++ b/src/components/Settings/index.js @@ -1,153 +1,122 @@ 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 { + correlationId: 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 + const values = { ...this.props.values } + values[field] = mapper[field](v, values[field], params) + this.setState(({ changedFields }) => { + const changedFieldsClone = changedFields.add(field) + return { changed: true, saved: false, changedFields: changedFieldsClone } + }, () => { + 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..cc1c0af657 100644 --- a/src/components/Settings/style.css +++ b/src/components/Settings/style.css @@ -11,6 +11,10 @@ .search-content-box { flex-grow: 1; + height: min-content; + max-width: 900px; + margin-left: auto; + margin-right: auto; } .main-content { @@ -36,4 +40,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 ba9afae5d8..b97328f469 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' @@ -53,6 +56,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..531192ff89 100644 --- a/src/components/UserSettings/GlobalSettings.js +++ b/src/components/UserSettings/GlobalSettings.js @@ -1,26 +1,15 @@ 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 style from './style.css' -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 +25,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 +68,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: [ @@ -244,111 +125,49 @@ class GlobalSettings extends Component { }, { title: msg.language(), - body: , + body:
    + +
    , }, - ], - }, - vm: [{ - title: msg.virtualMachines(), - tooltip: msg.globalSettingsWillBeApplied(), - fields: [ { - title: msg.uiRefresh(), - body: , + title: msg.sshKey(), + tooltip: msg.sshKeyTooltip(), + body:
    + onChange('sshKey')(e.target.value)} + value={values.sshKey} + /> +
    , }, ], }, - { - 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(), + refreshInterval: { + title: msg.refreshInterval(), tooltip: msg.globalSettingsWillBeApplied(), fields: [ { - title: msg.sshKey(), - tooltip: msg.sshKeyTooltip(), - body: this.handleChange('sshKey')(e.target.value)} - value={values.sshKey} - />, - }, - { - title: msg.fullScreenMode(), - body: this.handleChange('fullScreenMode')(state)} - />, - }, - { - title: msg.ctrlAltDel(), - tooltip: msg.ctrlAltDelTooltip(), - body: this.handleChange('ctrlAltDel')(state)} - />, - }, - { - title: msg.automaticConsoleConnection(), - body: - {msg.automaticConsoleConnectionDetails()} - , - }, - { - title: msg.smartcard(), - tooltip: msg.smartcardTooltip(), - body: - {msg.smartcardDetails()} - , + title: msg.uiRefresh(), + body:
    + +
    , }, ], }, notifications: { title: msg.notifications(), + tooltip: msg.notificationSettingsAffectAllNotifications(), fields: [ { title: msg.dontDisturb(), @@ -357,59 +176,38 @@ class GlobalSettings extends Component { bsSize='normal' title='normal' value={values.dontDisturb} - onChange={(e, state) => this.handleChange('dontDisturb')(state)} + onChange={(e, state) => onChange('dontDisturb')(state)} />, }, { title: msg.dontDisturbFor(), - body: , - }, - { - title: msg.disableVmNotifications(), - body: , + 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 +217,6 @@ GlobalSettings.propTypes = { options: PropTypes.object.isRequired, saveOptions: PropTypes.func.isRequired, goToMainPage: PropTypes.func.isRequired, - resetSettings: PropTypes.func.isRequired, } export default connect( @@ -429,8 +226,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..cd3facb461 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.isMultiSelect 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') }), + title: msg.confirmationMessages(), 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: (this.isMultiSelected || 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,111 @@ 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, + isMultiSelect: PropTypes.bool, 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..a0ac29d37f 100644 --- a/src/components/UserSettings/style.css +++ b/src/components/UserSettings/style.css @@ -3,27 +3,22 @@ overflow: auto; } -.alert-container { - position: absolute; - top: 5%; - left: 25%; - width: 50%; +.filter-vms-box { + margin-right: 20px; + margin-bottom: 10px; } -.toolbar :global(.toolbar-pf.row) { - padding-bottom: 10px; - margin-left: 0; +.vms-settings-box { + display: flex; } -.toolbar :global(.toolbar-pf-action-right) button { - margin-right: 10px; +.vms-card { + margin-left: 20px; + margin-top: 20px; + flex-grow: 1; + overflow: hidden; } -:global(#settings-toolbar) { - margin-left: -20px; +.half-width { + width: 50%; } - -.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 3db09be675..e3004cbf3e 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' @@ -15,12 +17,10 @@ import { startVm } from '_/actions' import { getOsHumanName, getVmIcon } from '../utils' import { enumMsg } from '_/intl' -import style from './style.css' - /** * Single icon-card in the list for a VM */ -const Vm = ({ vm, icons, os, vms, onStart }) => { +const Vm = ({ vm, checked, icons, os, vms, onStart, onCheck }) => { const idPrefix = `vm-${vm.get('name')}` const state = vm.get('status') const stateValue = enumMsg('VmStatus', state) @@ -36,6 +36,7 @@ const Vm = ({ vm, icons, os, vms, onStart }) => { return ( + {osName} {isPoolVm && pool && { pool.get('name') }} @@ -48,12 +49,15 @@ const Vm = ({ vm, icons, os, vms, onStart }) => { ) } + Vm.propTypes = { vm: PropTypes.object.isRequired, icons: PropTypes.object.isRequired, vms: 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 9c34f3fb02..7846c284c0 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().map(vm => vm.set('isVm', true)) 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() const vmsPoolsMerge = [ ...sortedVms, ...sortedPools ].sort(sortFunction(sort)) @@ -50,12 +123,19 @@ class Vms extends React.Component { loader={} useWindow={false} > + +
    {vmsPoolsMerge.map(instance => instance.get('isVm') - ? + ? this.checkVm(instance.get('id'))} + /> : )}
    diff --git a/src/components/VmsList/style.css b/src/components/VmsList/style.css index 939f211b6e..728f2bb831 100644 --- a/src/components/VmsList/style.css +++ b/src/components/VmsList/style.css @@ -101,4 +101,10 @@ dl.pool-info dd { .card-box :global(.card-pf) { height: calc(100% - 20px); -} \ No newline at end of file +} + +.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 44d1d592a2..ae420608ef 100644 --- a/src/index-nomodules.css +++ b/src/index-nomodules.css @@ -232,3 +232,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 4eae30bdfd..67d70d940e 100644 --- a/src/intl/messages.js +++ b/src/intl/messages.js @@ -18,6 +18,7 @@ export const messages: { [messageId: string]: MessageType } = { message: 'About', description: 'About application', }, + accountSettings: 'Account Settings', actionFailed: '{action} failed', activeFilters: 'Active Filters:', actualStateVmIsIn: 'The actual state the virtual machine is in.', @@ -38,7 +39,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.', @@ -62,6 +63,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', @@ -97,6 +99,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.', @@ -123,7 +126,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', @@ -423,6 +425,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', @@ -444,6 +448,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', @@ -459,6 +464,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', @@ -485,11 +491,13 @@ 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}.', + settings: 'Settings', shutdown: 'Shutdown', shutdownStatelessPoolVm: 'This virtual machine belongs to {poolName} and is stateless so any data that is currently attached to the virtual machine will be lost if it is shutdown. The virtual machine will be returned to {poolName} if shutdown.', shutdownVm: 'Shutdown the VM', @@ -599,6 +607,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', @@ -608,6 +617,7 @@ export const messages: { [messageId: string]: MessageType } = { vmMemory: 'VM Memory', vmPortal: 'VM Portal', vmPoolSnapshotRestoreUnavailable: 'This VM is from a pool, so this action is unavailable.', + vmSettings: 'VM Settings', vmType_desktop: 'Desktop', vmType_highPerformance: 'High Performance', vmType_server: 'Server', diff --git a/src/intl/translated-messages.json b/src/intl/translated-messages.json index 898af8f452..bbe0418afe 100644 --- a/src/intl/translated-messages.json +++ b/src/intl/translated-messages.json @@ -1,6 +1,5 @@ { "cs": { - "SSHKey": "SSH Klíč", "about": "O aplikaci", "actionFailed": "{action} se nezdařilo", "actualStateVmIsIn": "Skutečný stav virtuálního stroje.", @@ -20,7 +19,6 @@ "close": "Zavřít", "cloudInit": "Cloud-Init", "cluster": "Cluster", - "connectAutomatically": "Připojit automaticky", "console": "Konzole", "consoleInUseContinue": "Konzole je používána, pokračovat?", "containsConfigurationAndDisksWhichWillBeUsedToCreateThisVm": "Obsahuje nastavení a jednotky datového úložiště, které budou použity k vytvoření tohoto virtuálního stroje. Upravte si nastavení jak potřebujete.", @@ -156,7 +154,6 @@ "yes": "Ano" }, "de": { - "SSHKey": "SSH-Schlüssel", "about": "Über", "actionFailed": "{action} fehlgeschlagen", "actualStateVmIsIn": "Der tatsächliche Zustand, in dem sich die virtuelle Maschine befindet.", @@ -197,7 +194,6 @@ "confirmDelete": "Löschen bestätigen", "confirmRestore": "Wiederherstellen bestätigen", "connect": "Verbinden", - "connectAutomatically": "Automatisch verbinden", "console": "Konsole", "consoleInUseContinue": "Konsole in Gebrauch, fortfahren?", "containsConfigurationAndDisksWhichWillBeUsedToCreateThisVm": "Enthält die Konfigurierung und Disks, die verwendet werden, um diese virtuelle Maschine zu erstellen. Passen Sie sie nach Bedarf an.", @@ -482,7 +478,6 @@ "youHaveNoAllowedVnicProfiles": "Sie können NICs nicht erstellen oder bearbeiten, da Sie nicht die Berechtigung haben, vNIC-Profile im Data-Center der VM zu verwenden." }, "es": { - "SSHKey": "Clave SSH", "about": "Acerca", "actionFailed": "La acción {action} falló", "actualStateVmIsIn": "El estado actual en el que se encuentra la máquina virtual.", @@ -523,7 +518,6 @@ "confirmDelete": "Confirmar eliminación", "confirmRestore": "Confirmar restauración", "connect": "Conectar", - "connectAutomatically": "Conectar automáticamente", "console": "Consola", "consoleInUseContinue": "Consola en uso, ¿continuar?", "containsConfigurationAndDisksWhichWillBeUsedToCreateThisVm": "Contiene la configuración y discos que serán usados para crear esta máquina virtual. Por favor, realice las personalizaciones oportunas.", @@ -808,7 +802,6 @@ "youHaveNoAllowedVnicProfiles": "No puede crear o editar las NIC porque no tiene permiso para usar ningún perfil vNIC en el centro de datos de la MV." }, "fr": { - "SSHKey": "Clé SSH", "about": "À propos de", "actionFailed": "{action} a échoué", "actualStateVmIsIn": "L'état actuel de la machine virtuelle.", @@ -849,7 +842,6 @@ "confirmDelete": "Confirmer la suppression", "confirmRestore": "Confirmer la restauration", "connect": "Connexion", - "connectAutomatically": "Connexion automatique", "console": "Console", "consoleInUseContinue": "Console en cours d'utilisation, souhaitez-vous continuer ?", "containsConfigurationAndDisksWhichWillBeUsedToCreateThisVm": "Contient la configuration et les disques qui seront utilisés pour créer cette machine virtuelle. Veuillez personnaliser en fonction des besoins.", @@ -1267,7 +1259,6 @@ "yes": "Si" }, "ja": { - "SSHKey": "ユーザーの公開鍵", "about": "バージョン情報", "actionFailed": "{action} が失敗しました", "actualStateVmIsIn": "仮想マシンの実際の状態", @@ -1308,7 +1299,6 @@ "confirmDelete": "削除の確認", "confirmRestore": "復元の確認", "connect": "接続", - "connectAutomatically": "自動的に接続する", "console": "コンソール", "consoleInUseContinue": "コンソールは使用中です。操作を続行しますか?", "containsConfigurationAndDisksWhichWillBeUsedToCreateThisVm": "この仮想マシンの作成に使用される設定とディスクが含まれています。必要に応じてカスタマイズしてください。", @@ -1593,7 +1583,6 @@ "youHaveNoAllowedVnicProfiles": "仮想マシンのデータセンターで vNIC プロファイルを使用するパーミッションがないので、NIC を作成または編集することはできません。" }, "ko": { - "SSHKey": "SSH 키 ", "about": "정보 ", "actionFailed": "{action}에 실패했습니다 ", "actualStateVmIsIn": "가상 머신의 실제 상태입니다.", @@ -1634,7 +1623,6 @@ "confirmDelete": "삭제 확인", "confirmRestore": "복구 확인 ", "connect": "연결", - "connectAutomatically": "자동 연결", "console": "콘솔", "consoleInUseContinue": "콘솔이 사용 중입니다. 작업을 계속하시겠습니까? ", "containsConfigurationAndDisksWhichWillBeUsedToCreateThisVm": "이 가상 머신을 생성하는데 사용되는 설정 및 디스크가 포함되어 있습니다. 필요에 따라 사용자 정의하십시오.", @@ -1919,7 +1907,6 @@ "youHaveNoAllowedVnicProfiles": "가상 머신 데이터 센터에서 vNIC 프로파일을 사용할 수있는 권한이 없으므로 NIC를 만들거나 편집할 수 없습니다." }, "pt-BR": { - "SSHKey": "Chave SSH", "about": "Sobre", "actionFailed": "falha ao {action}", "actualStateVmIsIn": "O estado atual no qual a máquina virtual está.", @@ -1960,7 +1947,6 @@ "confirmDelete": "Confirmar exclusão", "confirmRestore": "Confirmar restauração", "connect": "Conectar", - "connectAutomatically": "Conectar automaticamente", "console": "Console", "consoleInUseContinue": "Console em uso, deseja continuar?", "containsConfigurationAndDisksWhichWillBeUsedToCreateThisVm": "Contém a configuração e os discos que serão usados para criar essa máquina virtual. Personalize conforme necessário.", @@ -2248,7 +2234,6 @@ "cd": "CD" }, "zh-CN": { - "SSHKey": "SSH 密钥", "about": "关于", "actionFailed": "{action} 失败", "actualStateVmIsIn": "虚拟机实际的状态。", @@ -2289,7 +2274,6 @@ "confirmDelete": "确认删除", "confirmRestore": "确认恢复", "connect": "连接", - "connectAutomatically": "自动连接", "console": "控制台", "consoleInUseContinue": "控制台在使用,需要继续?", "containsConfigurationAndDisksWhichWillBeUsedToCreateThisVm": "包括用来创建这个虚拟机的配置和磁盘。请根据需要对它们进行定制。", 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 9aec6332ad..b1342beed5 100644 --- a/src/routes.js +++ b/src/routes.js @@ -59,6 +59,18 @@ export default function getRoutes (vms) { closeable: true, type: DIALOG_PAGE_TYPE, }, + { + path: '/vms-settings/:id+', + title: msg.vmSettings(), + component: VmSettingsPage, + toolbars: () =>
    , + closeable: true, + isToolbarFullWidth: true, + type: VM_SETTINGS_PAGE_TYPE, + pageProps: { + isMultiSelect: true, + }, + }, { path: '/vm/:id', @@ -77,8 +89,8 @@ export default function getRoutes (vms) { type: CONSOLE_PAGE_TYPE, }, { - path: '/vm/:id/settings', - title: (match) => 'settings' || match.params.id, + path: '/vm/:id+/settings', + title: msg.settings(), component: VmSettingsPage, toolbars: () =>
    , closeable: true, @@ -91,7 +103,7 @@ export default function getRoutes (vms) { { path: '/settings', exact: true, - title: () => 'Settings', + title: msg.accountSettings(), component: GlobalSettingsPage, toolbars: () =>
    , closeable: true, diff --git a/src/sagas/index.js b/src/sagas/index.js index 1ced5b2c14..7d0192cab5 100644 --- a/src/sagas/index.js +++ b/src/sagas/index.js @@ -256,7 +256,10 @@ function* refreshConsolePage ({ 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..d8fa2db14c 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 isVmNotDisturb = yield select(state => state.options.getIn(['vms', action.payload.vmId, 'disturb'], true)) const isDontDisturb = yield select(state => state.options.getIn(['options', 'dontDisturb'], false)) - if (!canBeMissing && !(isDontDisturb && isFiltered)) { + if (!canBeMissing && !(isDontDisturb && isVmNotDisturb)) { console.log(`External action exception: ${JSON.stringify(e)}`) if (e.status === 401) { // Unauthorized