diff --git a/packaging/ovirt-web-ui.war/WEB-INF/web.xml b/packaging/ovirt-web-ui.war/WEB-INF/web.xml
index cbdcdfe4a7..b2b0a32765 100644
--- a/packaging/ovirt-web-ui.war/WEB-INF/web.xml
+++ b/packaging/ovirt-web-ui.war/WEB-INF/web.xml
@@ -83,6 +83,7 @@
logout
org.ovirt.engine.core.aaa.servlet.SsoLogoutServlet
+
logout
/sso/logout
@@ -124,6 +125,7 @@
index
/vm/*
/pool/*
+ /settings/*
diff --git a/src/actions/index.js b/src/actions/index.js
index dc51215272..5f921a54bc 100644
--- a/src/actions/index.js
+++ b/src/actions/index.js
@@ -1,10 +1,11 @@
-import AppConfiguration from '../config'
+import AppConfiguration from '_/config'
import {
APP_CONFIGURED,
CHANGE_PAGE,
CHECK_TOKEN_EXPIRED,
GET_BY_PAGE,
GET_OPTION,
+ GET_USER,
GET_USER_GROUPS,
MANUAL_REFRESH,
SET_ADMINISTRATOR,
@@ -13,6 +14,7 @@ import {
SET_DEFAULT_TIMEZONE,
SET_USB_AUTOSHARE,
SET_USB_FILTER,
+ SET_USER,
SET_USER_FILTER_PERMISSION,
SET_USER_GROUPS,
SET_USER_SESSION_TIMEOUT_INTERVAL,
@@ -194,6 +196,19 @@ export function getUserGroups () {
return { type: GET_USER_GROUPS }
}
+export function setUser ({ user }) {
+ return {
+ type: SET_USER,
+ payload: {
+ user,
+ },
+ }
+}
+
+export function getUser () {
+ return { type: GET_USER }
+}
+
export function setCpuTopologyOptions ({
maxNumberOfSockets,
maxNumberOfCores,
diff --git a/src/actions/options.js b/src/actions/options.js
index da4ae91ed3..cfa4cd553e 100644
--- a/src/actions/options.js
+++ b/src/actions/options.js
@@ -1,10 +1,24 @@
+// @flow
+
+import type { UserOptionsType, SshKeyType } from '_/ovirtapi/types'
+import type { LoadUserOptionsActionType, SaveGlobalOptionsActionType } from '_/actions/types'
+
import {
GET_CONSOLE_OPTIONS,
SAVE_CONSOLE_OPTIONS,
SET_CONSOLE_OPTIONS,
+ GET_SSH_KEY,
+ SAVE_GLOBAL_OPTIONS,
+ SAVE_SSH_KEY,
+ SET_SSH_KEY,
+ SET_OPTION,
+ LOAD_USER_OPTIONS,
+ LOAD_USER_OPTIONS_IN_PROGRESS,
+ LOAD_USER_OPTIONS_FINISHED,
+ PERSIST_OPTIONS,
} from '_/constants'
-export function setConsoleOptions ({ vmId, options }) {
+export function setConsoleOptions ({ vmId, options }: Object): Object {
return {
type: SET_CONSOLE_OPTIONS,
payload: {
@@ -14,7 +28,7 @@ export function setConsoleOptions ({ vmId, options }) {
}
}
-export function getConsoleOptions ({ vmId }) {
+export function getConsoleOptions ({ vmId }: Object): Object {
return {
type: GET_CONSOLE_OPTIONS,
payload: {
@@ -23,7 +37,7 @@ export function getConsoleOptions ({ vmId }) {
}
}
-export function saveConsoleOptions ({ vmId, options }) {
+export function saveConsoleOptions ({ vmId, options }: Object): Object {
return {
type: SAVE_CONSOLE_OPTIONS,
payload: {
@@ -32,3 +46,90 @@ export function saveConsoleOptions ({ vmId, options }) {
},
}
}
+
+export function getSSHKey ({ userId }: Object): Object {
+ return {
+ type: GET_SSH_KEY,
+ payload: {
+ userId,
+ },
+ }
+}
+
+export function setSSHKey ({ key, id }: SshKeyType): Object {
+ return {
+ type: SET_SSH_KEY,
+ payload: {
+ key,
+ id,
+ },
+ }
+}
+
+export function setOption ({ key, value }: Object): Object {
+ return {
+ type: SET_OPTION,
+ payload: {
+ key,
+ value,
+ },
+ }
+}
+
+export function loadUserOptions (userOptions: UserOptionsType): LoadUserOptionsActionType {
+ return {
+ type: LOAD_USER_OPTIONS,
+ payload: {
+ userOptions,
+ },
+ }
+}
+
+export function loadingUserOptionsInProgress (): Object {
+ return {
+ type: LOAD_USER_OPTIONS_IN_PROGRESS,
+ }
+}
+
+export function loadingUserOptionsFinished (): Object {
+ return {
+ type: LOAD_USER_OPTIONS_FINISHED,
+ }
+}
+
+export function saveGlobalOptions ({ values: { sshKey, language, showNotifications, notificationSnoozeDuration, updateRate } = {} }: Object, { transactionId }: Object): SaveGlobalOptionsActionType {
+ return {
+ type: SAVE_GLOBAL_OPTIONS,
+ payload: {
+ sshKey,
+ language,
+ showNotifications,
+ notificationSnoozeDuration,
+ updateRate,
+ },
+ meta: {
+ transactionId,
+ },
+ }
+}
+
+export function saveSSHKey ({ key, userId, sshId }: Object): Object {
+ return {
+ type: SAVE_SSH_KEY,
+ payload: {
+ key,
+ userId,
+ sshId,
+ },
+ }
+}
+
+export function persistUserOptions ({ options, userId }: Object): Object {
+ return {
+ type: PERSIST_OPTIONS,
+ payload: {
+ options,
+ userId,
+ },
+ }
+}
diff --git a/src/actions/types.js b/src/actions/types.js
new file mode 100644
index 0000000000..9b863d3e29
--- /dev/null
+++ b/src/actions/types.js
@@ -0,0 +1,24 @@
+// @flow
+import * as C from '_/constants'
+import type { UserOptionsType } from '_/ovirtapi/types'
+
+export type LoadUserOptionsActionType = {
+ type: C.LOAD_USER_OPTIONS,
+ payload: {
+ userOptions: UserOptionsType
+ }
+}
+
+export type SaveGlobalOptionsActionType = {
+ type: C.SAVE_GLOBAL_OPTIONS,
+ payload: {|
+ updateRate?: number,
+ language?: string,
+ showNotifications?: boolean,
+ notificationSnoozeDuration?: number,
+ sshKey?: string
+ |},
+ meta: {|
+ transactionId: string
+ |}
+}
diff --git a/src/components/OptionsDialog/actions.js b/src/components/OptionsDialog/actions.js
deleted file mode 100644
index 318852942e..0000000000
--- a/src/components/OptionsDialog/actions.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import { SET_SSH_KEY, GET_SSH_KEY, SAVE_SSH_KEY, SET_UNLOADED } from './constants'
-
-export function setSSHKey ({ key, id }) {
- return {
- type: SET_SSH_KEY,
- payload: {
- key,
- id,
- },
- }
-}
-
-export function saveSSHKey ({ key, userId, sshId }) {
- return {
- type: SAVE_SSH_KEY,
- payload: {
- key,
- userId,
- sshId,
- },
- }
-}
-
-export function getSSHKey ({ userId }) {
- return {
- type: GET_SSH_KEY,
- payload: {
- userId,
- },
- }
-}
-
-export function setUnloaded () {
- return { type: SET_UNLOADED }
-}
diff --git a/src/components/OptionsDialog/constants.js b/src/components/OptionsDialog/constants.js
deleted file mode 100644
index c7b52ef247..0000000000
--- a/src/components/OptionsDialog/constants.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export const GET_SSH_KEY = 'OPTIONS_DIALOG_HEADER_GET_SSH_KEY'
-export const SAVE_SSH_KEY = 'OPTIONS_DIALOG_HEADER_SAVE_SSH_KEY'
-export const SET_SSH_KEY = 'OPTIONS_DIALOG_HEADER_SET_SSH_KEY'
-export const SET_UNLOADED = 'OPTIONS_DIALOG_HEADER_SET_UNLOADED'
diff --git a/src/components/OptionsDialog/index.js b/src/components/OptionsDialog/index.js
deleted file mode 100644
index beef439759..0000000000
--- a/src/components/OptionsDialog/index.js
+++ /dev/null
@@ -1,122 +0,0 @@
-import React from 'react'
-import PropTypes from 'prop-types'
-import { connect } from 'react-redux'
-
-import { Modal } from 'patternfly-react'
-
-import FieldHelp from '../FieldHelp/index'
-
-import {
- getSSHKey,
- saveSSHKey,
-} from './actions'
-
-import { msg } from '_/intl'
-
-class OptionsDialog extends React.Component {
- constructor (props) {
- super(props)
- this.state = {
- sshKey: props.optionsDialog.get('sshKey') || '',
- openModal: false,
- }
- this.onSSHKeyChange = this.onSSHKeyChange.bind(this)
- this.onSaveClick = this.onSaveClick.bind(this)
- this.handleModalOpen = this.handleModalOpen.bind(this)
- }
-
- handleModalOpen () {
- if (this.props.userId) {
- this.props.getSSH()
- }
- this.setState({ 'sshKey': 'Loading...', openModal: true })
- }
-
- componentWillReceiveProps (nextProps) {
- if (nextProps.optionsDialog.get('loaded')) {
- this.setState({ 'sshKey': nextProps.optionsDialog.get('sshKey') })
- }
- }
-
- onSSHKeyChange (event) {
- this.setState({ sshKey: event.target.value })
- }
-
- onSaveClick () {
- this.props.onSave({ key: this.state.sshKey, sshId: this.props.optionsDialog.get('sshId') })
- this.setState({ openModal: false })
- }
-
- render () {
- const { oVirtApiVersion } = this.props
- const idPrefix = `optionsdialog`
-
- let content = (
-
- )
-
- if (!this.props.userId) {
- const apiVersion = oVirtApiVersion && oVirtApiVersion.get('major')
- ? `${oVirtApiVersion.get('major')}.${oVirtApiVersion.get('minor')}`
- : 'unknown'
-
- content = (
-
-
{msg.lowOVirtVersion({ apiVersion })}
-
- )
- }
- return (
-
-
- { this.state.openModal &&
- this.setState({ openModal: false })} show>
-
- this.setState({ openModal: false })} />
- {msg.options()}
-
-
- {content}
-
-
- {this.props.userId ? : null}
-
-
-
- }
-
- )
- }
-}
-
-OptionsDialog.propTypes = {
- userId: PropTypes.string,
- optionsDialog: PropTypes.object.isRequired,
- oVirtApiVersion: PropTypes.object,
- getSSH: PropTypes.func.isRequired,
- onSave: PropTypes.func.isRequired,
-}
-
-export default connect(
- (state) => ({
- optionsDialog: state.OptionsDialog,
- oVirtApiVersion: state.config.get('oVirtApiVersion'),
- }),
- (dispatch, { userId }) => ({
- getSSH: () => dispatch(getSSHKey({ userId })),
- onSave: ({ key, sshId }) => dispatch(saveSSHKey({ key, userId, sshId })),
- })
-)(OptionsDialog)
diff --git a/src/components/OptionsDialog/reducer.js b/src/components/OptionsDialog/reducer.js
deleted file mode 100644
index f72d9f0f7a..0000000000
--- a/src/components/OptionsDialog/reducer.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import Immutable from 'immutable'
-import { SET_SSH_KEY, SET_UNLOADED } from './constants'
-
-export function reducer (state, action) {
- state = state || Immutable.fromJS({ sshKey: null, sshId: undefined, loaded: false })
-
- switch (action.type) {
- case SET_SSH_KEY:
- return state.set('sshKey', action.payload.key).set('sshId', action.payload.id).set('loaded', true)
- case SET_UNLOADED:
- return state.set('loaded', false)
- default:
- return state
- }
-}
diff --git a/src/components/OptionsDialog/sagas.js b/src/components/OptionsDialog/sagas.js
deleted file mode 100644
index 0fed882db3..0000000000
--- a/src/components/OptionsDialog/sagas.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { put, takeEvery } from 'redux-saga/effects'
-import { SAVE_SSH_KEY, GET_SSH_KEY } from './constants'
-import { setSSHKey, setUnloaded } from './actions'
-import { callExternalAction } from '_/sagas/utils'
-import Api from '_/ovirtapi'
-
-function* saveSSHKey (action) {
- yield callExternalAction('saveSSHKey', Api.saveSSHKey, action)
-}
-
-function* getSSHKey (action) {
- yield put(setUnloaded())
- const result = yield callExternalAction('getSSHKey', Api.getSSHKey, action)
- if (result.error) {
- return
- }
- if (result.ssh_public_key && result.ssh_public_key.length > 0) {
- yield put(setSSHKey(Api.SSHKeyToInternal({ sshKey: result.ssh_public_key[0] })))
- } else {
- yield put(setSSHKey(Api.SSHKeyToInternal({ sshKey: '' })))
- }
-}
-
-export default [
- takeEvery(SAVE_SSH_KEY, saveSSHKey),
- takeEvery(GET_SSH_KEY, getSSHKey),
-]
diff --git a/src/components/Pages/index.js b/src/components/Pages/index.js
index 0ae5ce5ef1..87d0d12421 100644
--- a/src/components/Pages/index.js
+++ b/src/components/Pages/index.js
@@ -9,6 +9,7 @@ import VmsList from '../VmsList'
import VmDetails from '../VmDetails'
import VmConsole from '../VmConsole'
import Handler404 from '_/Handler404'
+import { GlobalSettings } from '../UserSettings'
/**
* Route component (for PageRouter) to view the list of VMs and Pools
@@ -17,6 +18,10 @@ const VmsListPage = () => {
return
}
+const GlobalSettingsPage = () => {
+ return
+}
+
/**
* Route component (for PageRouter) to view a VM's details
*/
@@ -105,4 +110,5 @@ export {
VmConsolePageConnected as VmConsolePage,
VmDetailsPageConnected as VmDetailsPage,
VmsListPage,
+ GlobalSettingsPage,
}
diff --git a/src/components/Settings/Settings.js b/src/components/Settings/Settings.js
new file mode 100644
index 0000000000..daf81a01f3
--- /dev/null
+++ b/src/components/Settings/Settings.js
@@ -0,0 +1,140 @@
+import React, { useState, useEffect } from 'react'
+import PropTypes from 'prop-types'
+
+import SettingsToolbar from './SettingsToolbar'
+import NavigationPrompt from 'react-router-navigation-prompt'
+import NavigationConfirmationModal from '../NavigationConfirmationModal'
+import CounterAlert from '_/components/CounterAlert'
+import { generateUnique } from '_/helpers'
+import { msg } from '_/intl'
+import style from './style.css'
+
+const changedInTheMeantime = ({ currentValues = {}, baseValues = {}, draftValues = {}, sentValues = {} }) => {
+ return Object.keys(currentValues).filter(name =>
+ currentValues[name] !== baseValues[name] && !(
+ // draft is the same as 3rd party change - no risk of losing data
+ currentValues[name] === draftValues[name] ||
+ // it's your own update but user modified draft in the meantime
+ currentValues[name] === sentValues[name]))
+}
+
+const pendingUserChanges = ({ currentValues = {}, draftValues = {} }) => {
+ return Object.keys(currentValues).filter(name =>
+ currentValues[name] !== draftValues[name] &&
+ draftValues[name] !== undefined
+ )
+}
+
+const changedInLastTransaction = ({ currentValues = {}, sentValues = {} }) => {
+ return Object.keys(currentValues).filter(name =>
+ currentValues[name] === sentValues[name] && currentValues[name] !== undefined)
+}
+
+const stillPending = ({ currentValues = {}, sentValues = {} }) => {
+ return Object.keys(currentValues).filter(name =>
+ sentValues[name] !== undefined &&
+ currentValues[name] !== sentValues[name])
+}
+
+const Settings = ({ draftValues, onSave, lastTransactionId, onCancel,
+ translatedLabels, baseValues, sentValues, currentValues,
+ resetBaseValues, children }) => {
+ const [transactionId, setTransactionId] = useState(null)
+
+ const handleSave = () => {
+ const saveFields = pendingUserChanges({ currentValues, draftValues }).reduce((acc, cur) => ({ ...acc, [cur]: draftValues[cur] }), {})
+ const id = generateUnique('Settings-save_')
+ setTransactionId(id)
+ onSave(saveFields, id)
+ }
+
+ const conflictingChanges = changedInTheMeantime({ currentValues, baseValues, draftValues, sentValues }).map(field => translatedLabels[field])
+ const pendingChanges = pendingUserChanges({ currentValues, draftValues })
+ const stillPendingAfterSave = stillPending({ currentValues, sentValues })
+ const changedInLastTrans = changedInLastTransaction({ currentValues, sentValues })
+
+ if (conflictingChanges.length) {
+ console.warn(`Store content changed while editing settings for fields: ${JSON.stringify(conflictingChanges)}`)
+ }
+
+ const fullSuccess = changedInLastTrans.length !== 0 &&
+ stillPendingAfterSave.length === 0 &&
+ transactionId === lastTransactionId
+ const completeFailure = changedInLastTrans.length === 0 &&
+ stillPendingAfterSave.length !== 0 &&
+ transactionId === lastTransactionId
+ const partialSuccess = changedInLastTrans.length !== 0 &&
+ stillPendingAfterSave.length !== 0 &&
+ transactionId === lastTransactionId
+
+ const [showFullSuccess, setShowFullSuccess] = useState(false)
+ const [showCompleteFailure, setShowCompleteFailure] = useState(false)
+ const [partialSave, setShowPartialSave] = useState({ show: false, fields: [] })
+
+ useEffect(() => {
+ const partialSaveState = {
+ show: partialSuccess,
+ fields: pendingChanges.map(e => {translatedLabels[e]}
),
+ }
+ if (partialSaveState.show) { setShowPartialSave(partialSaveState) }
+ if (completeFailure) { setShowCompleteFailure(completeFailure) }
+ if (fullSuccess) { setShowFullSuccess(fullSuccess) }
+ if (fullSuccess || completeFailure || partialSuccess) {
+ // the transaction has finished - remove tracking id
+ setTransactionId(null)
+ // reset to new base level that contains last modifications
+ resetBaseValues()
+ }
+ }, [partialSuccess, completeFailure, fullSuccess, pendingChanges])
+
+ return
+ !!pendingChanges.length}>
+ {({ isActive, onConfirm, onCancel }) => (
+
+ )}
+
+
+ { showFullSuccess &&
+ setShowFullSuccess(false)}
+ />
+ }
+ { partialSave.show &&
+ {msg.failedToSaveChangesToFields()}}
+ onDismiss={() => setShowPartialSave({ show: false, fields: [] })} >
+ {partialSave.fields}
+
+ }
+ { showCompleteFailure &&
+ setShowCompleteFailure(false)}
+ />
+ }
+
+
+ {children}
+
+}
+Settings.propTypes = {
+ draftValues: PropTypes.object.isRequired,
+ currentValues: PropTypes.object.isRequired,
+ baseValues: PropTypes.object.isRequired,
+ sentValues: PropTypes.object.isRequired,
+ translatedLabels: PropTypes.object.isRequired,
+ lastTransactionId: PropTypes.string,
+ onSave: PropTypes.func.isRequired,
+ onCancel: PropTypes.func.isRequired,
+ children: PropTypes.node,
+ resetBaseValues: PropTypes.func.isRequired,
+}
+
+export default Settings
diff --git a/src/components/Settings/SettingsBase.js b/src/components/Settings/SettingsBase.js
new file mode 100644
index 0000000000..0675817945
--- /dev/null
+++ b/src/components/Settings/SettingsBase.js
@@ -0,0 +1,83 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import {
+ Card,
+ Col,
+ ControlLabel,
+ FormGroup,
+} from 'patternfly-react'
+import { InfoTooltip } from '_/components/tooltips'
+
+import style from './style.css'
+
+const LabelCol = ({ children, tooltip, fieldPath, ...props }) => {
+ return
+ { children } { tooltip && }
+
+}
+LabelCol.propTypes = {
+ children: PropTypes.node.isRequired,
+ tooltip: PropTypes.string,
+ fieldPath: 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,
+}
+
+const SettingsBase = ({ sections }) => {
+ const existingSections = Object.entries(sections).filter(([key, section]) => !!section)
+ return (
+
+ { existingSections.map(([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..b8008f0800
--- /dev/null
+++ b/src/components/Settings/SettingsToolbar.js
@@ -0,0 +1,53 @@
+import React, { useEffect, useState } 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, enableSave }) => {
+ const [container] = useState(document.createElement('div'))
+ useEffect(() => {
+ const root = document.getElementById('settings-toolbar')
+ if (root) {
+ root.appendChild(container)
+ }
+ return () => root && root.removeChild(container)
+ })
+
+ return ReactDOM.createPortal(
+
+
+
+
+
+ ,
+ container
+ )
+}
+
+SettingsToolbar.propTypes = {
+ onSave: PropTypes.func.isRequired,
+ onCancel: PropTypes.func.isRequired,
+ enableSave: PropTypes.bool,
+}
+
+export default SettingsToolbar
diff --git a/src/components/Settings/index.js b/src/components/Settings/index.js
new file mode 100644
index 0000000000..59a1266a5f
--- /dev/null
+++ b/src/components/Settings/index.js
@@ -0,0 +1,2 @@
+export { default as Settings } from './Settings'
+export { default as SettingsBase } from './SettingsBase'
diff --git a/src/components/Settings/style.css b/src/components/Settings/style.css
new file mode 100644
index 0000000000..c55cbc6944
--- /dev/null
+++ b/src/components/Settings/style.css
@@ -0,0 +1,63 @@
+.settings-box {
+ display: flex;
+ margin-top: 20px;
+}
+
+.navigation-content {
+ padding: 0;
+ margin-right: 30px;
+ display: table;
+}
+
+.search-content-box {
+ flex-grow: 1;
+ height: min-content;
+ max-width: 900px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.main-content {
+ margin-top: 20px;
+ margin-left: 0;
+ margin-right: 0;
+}
+
+.main-content-container {
+ display: grid;
+}
+
+.field-label {
+ text-align: right;
+}
+
+.settings-field {
+ padding-top: 20px;
+ padding-bottom: 20px;
+ border-bottom: solid 1px #0000001f;
+ margin-left: -20px;
+ margin-right: -20px;
+ padding-left: 20px;
+ padding-right: 20px;
+ margin-bottom: 0;
+}
+
+.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/index.js b/src/components/Toolbar/index.js
index ba9afae5d8..596a1e68a0 100644
--- a/src/components/Toolbar/index.js
+++ b/src/components/Toolbar/index.js
@@ -73,8 +73,11 @@ const VmConsoleToolbarConnected = connect(
})
)(VmConsoleToolbar)
+const SettingsToolbar = () =>
+
export {
VmDetailToolbarConnected as VmDetailToolbar,
VmConsoleToolbarConnected as VmConsoleToolbar,
VmsListToolbar,
+ SettingsToolbar,
}
diff --git a/src/components/UserSettings/GlobalSettings.js b/src/components/UserSettings/GlobalSettings.js
new file mode 100644
index 0000000000..28b79dc8f0
--- /dev/null
+++ b/src/components/UserSettings/GlobalSettings.js
@@ -0,0 +1,255 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+
+import { connect } from 'react-redux'
+import { push } from 'connected-react-router'
+import { saveGlobalOptions } from '_/actions'
+import { FormControl, Switch } from 'patternfly-react'
+import { msg } from '_/intl'
+import style from './style.css'
+
+import { Settings, SettingsBase } from '../Settings'
+import SelectBox from '../SelectBox'
+import moment from 'moment'
+
+class GlobalSettings extends Component {
+ dontDisturbList = [
+ {
+ id: 10,
+ value: moment.duration(10, 'minutes').humanize(),
+ },
+ {
+ id: 60,
+ value: moment.duration(1, 'hours').humanize(),
+ },
+ {
+ id: 60 * 24,
+ value: moment.duration(1, 'days').humanize(),
+ },
+ {
+ id: Number.MAX_SAFE_INTEGER,
+ value: msg.sessionDuration(),
+ },
+ ]
+
+ updateRateList = [
+ {
+ id: 30,
+ value: msg.every30Seconds(),
+ },
+ {
+ id: 60,
+ value: msg.everyMinute(),
+ },
+ {
+ id: 120,
+ value: msg.every2Minute(),
+ },
+ {
+ id: 300,
+ value: msg.every5Minute(),
+ },
+ ]
+
+ constructor (props) {
+ super(props)
+ /**
+ * Typical flow (happy path):
+ * 1. at the begining:
+ * baseValues == draftValues == currentValues
+ * 2. after user edit:
+ * baseValues == currentValues
+ * BUT
+ * baseValues != draftValues
+ * 3. after 'save' but before action finished:
+ * baseValues == currentValues
+ * AND
+ * baseValue + sentValues == draftValues
+ * 4. successful 'save' triggers re-basing (back to step 1.)
+ */
+ this.state = {
+ // editable by the user (used by the widgets)
+ // represent the current state of user work
+ draftValues: {
+ ...props.currentValues,
+ },
+ // state before editing
+ // allows to detect changes by comparing values (baseValues - draftValues == changes)
+ // note that it's perfectly legal to have: baseValues != currentValues
+ // store can change i.e. after fetching data from the server
+ // or after some action i.e. 'do not disturb' expired
+ baseValues: {
+ ...props.currentValues,
+ },
+ // values submitted using 'save' action
+ // inlcude both remote(server and store) or local(store only)
+ sentValues: {},
+ // required for error handling: the case of partial success(only some fields saved)
+ // the alert shows the names of the fields that were NOT saved
+ translatedLabels: {
+ sshKey: msg.sshKey(),
+ language: msg.language(),
+ showNotifications: msg.dontDisturb(),
+ notificationSnoozeDuration: msg.dontDisturbFor(),
+ updateRate: msg.uiRefresh(),
+ },
+ }
+ this.handleCancel = this.handleCancel.bind(this)
+ this.buildSections = this.buildSections.bind(this)
+ this.saveOptions = this.saveOptions.bind(this)
+ this.resetBaseValues = this.resetBaseValues.bind(this)
+ this.onChange = this.onChange.bind(this)
+ }
+
+ resetBaseValues () {
+ const { currentValues } = this.props
+ this.setState({
+ sentValues: {},
+ baseValues: { ...currentValues },
+ })
+ }
+
+ saveOptions (values, transactionId) {
+ this.props.saveOptions(values, transactionId)
+ this.setState({
+ sentValues: { ...values },
+ })
+ }
+
+ handleCancel () {
+ this.props.goToMainPage()
+ }
+
+ onChange (field) {
+ return (value) => {
+ this.setState((state) => ({
+ draftValues: {
+ ...state.draftValues,
+ [field]: value,
+ },
+ }))
+ }
+ }
+
+ buildSections (onChange) {
+ const { draftValues, translatedLabels } = this.state
+ const { config } = this.props
+ const idPrefix = 'global-user-settings'
+ return {
+ general: {
+ title: msg.general(),
+ fields: [
+ {
+ title: msg.username(),
+ body: {config.userName},
+ },
+ {
+ title: msg.email(),
+ body: {config.email},
+ },
+ {
+ title: translatedLabels.sshKey,
+ tooltip: msg.sshKeyTooltip(),
+ body: (
+
+ onChange('sshKey')(e.target.value)}
+ value={draftValues.sshKey || ''}
+ rows={8}
+ />
+
+ ),
+ },
+ ],
+ },
+ notifications: {
+ title: msg.notifications(),
+ tooltip: msg.notificationSettingsAffectAllNotifications(),
+ fields: [
+ {
+ title: translatedLabels.showNotifications,
+ body: (
+ {
+ onChange('showNotifications')(!dontDisturb)
+ }}
+ />
+ ),
+ },
+ {
+ title: translatedLabels.notificationSnoozeDuration,
+ body: (
+
+
+
+ ),
+ },
+ ],
+ },
+ }
+ }
+
+ render () {
+ const { lastTransactionId, currentValues } = this.props
+ const { draftValues, baseValues, sentValues, translatedLabels } = this.state
+
+ return (
+
+
+
+
+
+ )
+ }
+}
+GlobalSettings.propTypes = {
+ currentValues: PropTypes.object.isRequired,
+ config: PropTypes.object.isRequired,
+ lastTransactionId: PropTypes.string,
+ saveOptions: PropTypes.func.isRequired,
+ goToMainPage: PropTypes.func.isRequired,
+}
+
+export default connect(
+ ({ options, config }) => ({
+ config: {
+ userName: config.getIn(['user', 'name']),
+ email: config.getIn(['user', 'email']),
+ },
+ currentValues: {
+ sshKey: options.getIn(['ssh', 'key']),
+ language: options.getIn(['global', 'language']),
+ showNotifications: options.getIn(['global', 'showNotifications']),
+ notificationSnoozeDuration: options.getIn(['global', 'notificationSnoozeDuration']),
+ updateRate: options.getIn(['global', 'updateRate']),
+ },
+ lastTransactionId: options.getIn(['lastTransactions', 'global', 'transactionId'], ''),
+ }),
+
+ (dispatch) => ({
+ saveOptions: (values, transactionId) => dispatch(saveGlobalOptions({ values }, { transactionId })),
+ goToMainPage: () => dispatch(push('/')),
+ })
+)(GlobalSettings)
diff --git a/src/components/UserSettings/index.js b/src/components/UserSettings/index.js
new file mode 100644
index 0000000000..5df2867c77
--- /dev/null
+++ b/src/components/UserSettings/index.js
@@ -0,0 +1 @@
+export { default as GlobalSettings } from './GlobalSettings'
diff --git a/src/components/UserSettings/style.css b/src/components/UserSettings/style.css
new file mode 100644
index 0000000000..41ba692426
--- /dev/null
+++ b/src/components/UserSettings/style.css
@@ -0,0 +1,3 @@
+.half-width {
+ width: 50%;
+}
\ No newline at end of file
diff --git a/src/components/VmsPageHeader/UserMenu.js b/src/components/VmsPageHeader/UserMenu.js
index a6699989cf..a87796721a 100644
--- a/src/components/VmsPageHeader/UserMenu.js
+++ b/src/components/VmsPageHeader/UserMenu.js
@@ -6,7 +6,6 @@ import { logout } from '_/actions'
import { msg } from '_/intl'
import AboutDialog from '../About'
-import OptionsDialog from '../OptionsDialog'
import { Tooltip } from '_/components/tooltips'
const UserMenu = ({ config, onLogout }) => {
@@ -19,9 +18,6 @@ const UserMenu = ({ config, onLogout }) => {
- -
-
-
-
diff --git a/src/components/VmsPageHeader/index.js b/src/components/VmsPageHeader/index.js
index 8b57496111..b1ff9848af 100644
--- a/src/components/VmsPageHeader/index.js
+++ b/src/components/VmsPageHeader/index.js
@@ -2,6 +2,8 @@ import React, { useState } 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 Bellicon from '../VmUserMessages/Bellicon'
@@ -32,6 +34,16 @@ const VmsPageHeader = ({ onRefresh }) => {
+ -
+
+
+
+
+
+
setShow(!show)} />
diff --git a/src/config.js b/src/config.js
index 34b040e3c0..7c8d038c0c 100644
--- a/src/config.js
+++ b/src/config.js
@@ -12,6 +12,7 @@ const AppConfiguration = {
applicationLogoutURL: '', // url to invalidate the user's SSO token ('' skips SSO token invalidation)
pageLimit: 20,
schedulerFixedDelayInSeconds: 60,
+ notificationSnoozeDurationInMinutes: 10,
consoleClientResourcesURL: 'https://www.ovirt.org/documentation/admin-guide/virt/console-client-resources/',
cockpitPort: '9090',
diff --git a/src/constants/index.js b/src/constants/index.js
index 14f99e3ee5..9f09f40121 100644
--- a/src/constants/index.js
+++ b/src/constants/index.js
@@ -21,6 +21,7 @@ export const DELAYED_REMOVE_ACTIVE_REQUEST = 'DELAYED_REMOVE_ACTIVE_REQUEST'
export const DELETE_VM_NIC = 'DELETE_VM_NIC'
export const DISMISS_USER_MSG = 'DISMISS_USER_MSG'
export const DISMISS_EVENT = 'DISMISS_EVENT'
+export const DONT_DISTURB = 'DONT_DISTURB'
export const DOWNLOAD_CONSOLE_VM = 'DOWNLOAD_CONSOLE_VM'
export const EDIT_VM = 'EDIT_VM'
export const EDIT_VM_DISK = 'EDIT_VM_DISK'
@@ -37,14 +38,19 @@ export const GET_BY_PAGE = 'GET_BY_PAGE'
export const GET_CONSOLE_OPTIONS = 'GET_CONSOLE_OPTIONS'
export const GET_ISO_FILES = 'GET_ISO_FILES'
export const GET_OPTION = 'GET_OPTION'
+export const GET_SSH_KEY = 'GET_SSH_KEY'
export const GET_POOL = 'GET_POOL'
export const GET_POOLS = 'GET_POOLS'
export const GET_RDP_VM = 'GET_RDP_VM'
export const GET_ROLES = 'GET_ROLES'
+export const GET_USER = 'GET_USER'
export const GET_USER_GROUPS = 'GET_USER_GROUPS'
export const GET_VM = 'GET_VM'
export const GET_VM_CDROM = 'GET_VM_CDROM'
export const GET_VMS = 'GET_VMS'
+export const LOAD_USER_OPTIONS = 'LOAD_USER_OPTIONS'
+export const LOAD_USER_OPTIONS_FINISHED = 'LOAD_USER_OPTIONS_FINISHED'
+export const LOAD_USER_OPTIONS_IN_PROGRESS = 'LOAD_USER_OPTIONS_IN_PROGRESS'
export const LOGIN = 'LOGIN'
export const LOGIN_FAILED = 'LOGIN_FAILED'
export const LOGIN_SUCCESSFUL = 'LOGIN_SUCCESSFUL'
@@ -52,6 +58,7 @@ export const LOGOUT = 'LOGOUT'
export const MANUAL_REFRESH = 'MANUAL_REFRESH'
export const MAX_VM_MEMORY_FACTOR = 4 // see Edit VM flow; magic constant to stay aligned with Web Admin
export const OPEN_CONSOLE_VM = 'OPEN_CONSOLE_VM'
+export const PERSIST_OPTIONS = 'PERSIST_OPTIONS'
export const POOL_ACTION_IN_PROGRESS = 'POOL_ACTION_IN_PROGRESS'
export const REDIRECT = 'REDIRECT'
export const REFRESH_DATA = 'REFRESH_DATA'
@@ -70,6 +77,8 @@ export const REMOVE_VMS = 'REMOVE_VMS'
export const RESTART_VM = 'RESTART_VM'
export const SAVE_CONSOLE_OPTIONS = 'SAVE_CONSOLE_OPTIONS'
export const SAVE_FILTERS = 'SAVE_FILTERS'
+export const SAVE_GLOBAL_OPTIONS = 'SAVE_GLOBAL_OPTIONS'
+export const SAVE_SSH_KEY = 'SAVE_SSH_KEY'
export const SELECT_POOL_DETAIL = 'SELECT_POOL_DETAIL'
export const SELECT_VM_DETAIL = 'SELECT_VM_DETAIL'
export const SET_ADMINISTRATOR = 'SET_ADMINISTRATOR'
@@ -88,13 +97,16 @@ export const SET_DEFAULT_TIMEZONE = 'SET_DEFAULT_TIMEZONE'
export const SET_FILTERS = 'SET_FILTERS'
export const SET_HOSTS = 'SET_HOSTS'
export const SET_OPERATING_SYSTEMS = 'SET_OPERATING_SYSTEMS'
+export const SET_OPTION = 'SET_OPTION'
export const SET_OVIRT_API_VERSION = 'SET_OVIRT_API_VERSION'
export const SET_ROLES = 'SET_ROLES'
+export const SET_SSH_KEY = 'SET_SSH_KEY'
export const SET_STORAGE_DOMAIN_FILES = 'SET_STORAGE_DOMAIN_FILES'
export const SET_STORAGE_DOMAINS = 'SET_STORAGE_DOMAINS'
export const SET_TEMPLATES = 'SET_TEMPLATES'
export const SET_USB_AUTOSHARE = 'SET_USB_AUTOSHARE'
export const SET_USB_FILTER = 'SET_USB_FILTER'
+export const SET_USER = 'SET_USER'
export const SET_USER_FILTER_PERMISSION = 'SET_USER_FILTER_PERMISSION'
export const SET_USER_GROUPS = 'SET_USER_GROUPS'
export const SET_USER_SESSION_TIMEOUT_INTERVAL = 'SET_USER_SESSION_TIMEOUT_INTERVAL'
diff --git a/src/constants/pages.js b/src/constants/pages.js
index e28e69fd12..ad2a897b9f 100644
--- a/src/constants/pages.js
+++ b/src/constants/pages.js
@@ -4,3 +4,4 @@ export const DETAIL_PAGE_TYPE = 'detailPage'
export const LIST_PAGE_TYPE = 'listPage'
export const NO_REFRESH_TYPE = 'noRefreshPage'
export const EMPTY_VNIC_PROFILE_ID = ''
+export const SETTINGS_PAGE_TYPE = 'settingsPage'
diff --git a/src/intl/index.js b/src/intl/index.js
index 0af8cdbcab..634122fdc6 100644
--- a/src/intl/index.js
+++ b/src/intl/index.js
@@ -5,16 +5,13 @@ import { initIntl } from './initialize'
import { messages, type MessageIdType, type MessageType } from './messages'
import translatedMessages from './translated-messages.json'
+import localeWithFullName from './localeWithFullName.json'
export const DEFAULT_LOCALE: string = 'en'
export const DUMMY_LOCALE: string = 'aa' // NOTE: Used for development and testing
-export const BASE_LOCALE_SET: Set = new Set([
- DEFAULT_LOCALE,
- 'cs', 'de', 'es', 'fr', 'it', 'ja', 'ko', 'pt-BR', 'zh-CN',
-])
-
+export const BASE_LOCALE_SET: Set = new Set(Object.keys(localeWithFullName))
/**
* Currently selected locale
*/
diff --git a/src/intl/localeWithFullName.json b/src/intl/localeWithFullName.json
new file mode 100644
index 0000000000..ed25afc399
--- /dev/null
+++ b/src/intl/localeWithFullName.json
@@ -0,0 +1,13 @@
+{
+ "cs" : "Česky",
+ "de": "Deutsch - Deutschland",
+ "en":"U.S. English",
+ "es":"español - España",
+ "fr":"français - France",
+ "it":"italiano - Italia",
+ "ja":"日本語 - 日本",
+ "ko":"한국어 - 대한민국",
+ "pt-BR":"português do Brasil",
+ "zh-CN":"中文(简体) - 中国"
+}
+
diff --git a/src/intl/messages.js b/src/intl/messages.js
index 4901bb6b61..753a9b9b70 100644
--- a/src/intl/messages.js
+++ b/src/intl/messages.js
@@ -23,6 +23,7 @@ export const messages: { [messageId: string]: MessageType } = {
aboutDialogDocumentationText: 'Documentation',
aboutDialogReportIssuesLink: 'Please report issues on {link}',
aboutDialogVersion: 'Version {version}',
+ accountSettings: 'Account Settings',
actionFailed: '{action} failed',
actionFeedbackShutdownVm: 'Request to shutdown VM - {VmName} has been received.',
actionFeedbackRestartVm: 'Request to restart VM - {VmName} has been received.',
@@ -65,6 +66,10 @@ export const messages: { [messageId: string]: MessageType } = {
cd: 'CD',
cdCanOnlyChangeWhenVmRunning: 'CD can only be changed when the VM is running.',
cdromBoot: 'CD-ROM',
+ changesSavedSuccesfully: {
+ message: 'Changes to settings saved succesfully!',
+ description: 'Message displayed when all user settings have been saved successfully',
+ },
clear: 'Clear',
clearAll: 'Clear all',
clearAllFilters: 'Clear All Filters',
@@ -218,6 +223,8 @@ export const messages: { [messageId: string]: MessageType } = {
disks: 'Disks',
diskSizeHasToBeAPositiveInteger: 'Disk size has to be a positive integer.',
displayAll: 'Display all',
+ dontDisturb: 'Do not disturb',
+ dontDisturbFor: 'Do not disturb for',
downloadVirtManagerMSI: 'Download the MSI from virt-manager.org',
downloadVVFile: 'Download VV File',
downloadedSPICE: 'The VV file has been downloaded. Select the file to view the SPICE console on a desktop viewer.',
@@ -229,6 +236,7 @@ export const messages: { [messageId: string]: MessageType } = {
editDisk: 'Edit Disk',
editNic: 'Edit NIC',
editVm: 'Edit the VM',
+ email: 'Email',
empty: 'Empty',
emptySnapshotDescription: 'Snapshot description is missing.',
enterVmDescription: 'Enter VM Description (optional)',
@@ -317,6 +325,10 @@ export const messages: { [messageId: string]: MessageType } = {
description: 'VM is not responding. One of states of a virtual machine. Other are e.g. Up, Down, Powering-Up',
},
errorWhileCreatingNewDisk: 'Error while creating new disk:',
+ every30Seconds: 'Every 30 seconds',
+ everyMinute: 'Every minute',
+ every2Minute: 'Every 2 minutes',
+ every5Minute: 'Every 5 minutes',
failedToChangeVmIcon: 'Failed to change VM icon',
failedToChangeVmIconToDefault: 'Failed to change VM icon to default',
failedToGetVmConsole: 'Failed to get the VM console',
@@ -329,6 +341,14 @@ export const messages: { [messageId: string]: MessageType } = {
failedToRetrieveVmDetails: 'Failed to retrieve VM details',
failedToRetrieveVmDisks: 'Failed to retrieve VM disks',
failedToRetrieveVmIcon: 'Failed to retrieve VM icon',
+ failedToSaveChangesToFields: {
+ message: 'Failed to save changes to:',
+ description: 'Some fields have not been saved. List of fields is displayed below in a horizontal list',
+ },
+ failedToSaveChanges: {
+ message: 'Failed to save changes to settings',
+ description: 'No fields have been saved',
+ },
failedToShutdownVm: 'Failed to shutdown the VM',
failedToStartVm: 'Failed to start the VM',
failedToSuspendVm: 'Failed to suspend the VM',
@@ -339,6 +359,7 @@ export const messages: { [messageId: string]: MessageType } = {
freeBrowsers: 'Free browsers:',
fullScreen: 'Full Screen',
fullyQualifiedDomainName: 'Fully Qualified Domain Name (FQDN) of the virtual machine. Please note, guest agent must be installed within the virtual machine to retrieve this value.',
+ general: 'General',
gitHub: 'GitHub',
globalErrorBoundaryTitle: 'Sorry, VM Portal is currently having some issues.',
globalErrorBoundaryDescription: 'Please refresh the page or log out and log back in. If the issue persists, please report a bug on {bugUrl}',
@@ -369,6 +390,7 @@ export const messages: { [messageId: string]: MessageType } = {
ipAddress: { message: 'IP Address', description: 'Label for IP addresses reported by VM guest agent' },
isPersistMemorySnapshot: 'Content of the memory of the virtual machine is included in the snapshot.',
itemDoesntExistOrDontHavePermissions: 'The item doesn\'t exist or you do not have the permissions to view it.',
+ language: 'Language',
less: {
message: 'less',
description: 'more/less pair used to control collapsible long listing',
@@ -461,8 +483,9 @@ export const messages: { [messageId: string]: MessageType } = {
message: 'This field is only available when the VM is running and the guest agent is installed and running.',
description: 'Tooltip displayed next to \'notAvailable\' for fields that require the VM to be up and a running guest agent',
},
- notifications: 'Notifications',
notEditableForPoolsOrPoolVms: 'Not editable for Pools or pool VMs.',
+ notifications: 'Notifications',
+ notificationSettingsAffectAllNotifications: 'Notification settings applied here affect all notifications.',
noVmAvailable: 'No VM available.',
noVmAvailableForLoggedUser: 'No VM is available for the logged user.',
off: 'Off',
@@ -501,6 +524,8 @@ export const messages: { [messageId: string]: MessageType } = {
message: 'Refresh',
description: 'Reload data from server',
},
+ refreshInterval: 'Refresh Interval',
+ refreshIntervalTooltip: 'Interval at which each screen gets refreshed.',
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',
@@ -530,6 +555,7 @@ export const messages: { [messageId: string]: MessageType } = {
message: 'Your session is about to timeout due to inactivity.',
description: 'Primary message for SessionTimeout modal component',
},
+ sessionDuration: 'session duration',
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',
@@ -550,7 +576,8 @@ export const messages: { [messageId: string]: MessageType } = {
snapshotsTooltip: 'VM snapshots.',
sshAuthorizedKeys: 'SSH Authorized Keys',
sshAuthorizedKeysTooltip: 'New line separated public SSH keys allowing for passwordless remote login.',
- SSHKey: 'SSH Key',
+ sshKey: 'SSH Key',
+ sshKeyTooltip: 'This public key provides access to the guest serial console via SSH authentication.',
spiceConsole: 'SPICE Console',
startVmOnCreation: 'Start virtual machine on creation',
state: 'State',
@@ -590,6 +617,7 @@ export const messages: { [messageId: string]: MessageType } = {
totalMemoryVmWillBeEquippedWith: 'Total memory the virtual machine will be equipped with.',
troubleWithFindingPage: 'We\'re having trouble finding that page.',
typeOfWorkloadVmConfigurationIsOptimizedFor: 'Type of workload the virtual machine configuration is optimized for.',
+ uiRefresh: 'UI refresh',
uniqueNameOfTheVirtualMachine: 'Unique name of the virtual machine.',
unknown: {
message: 'unknown',
@@ -655,6 +683,7 @@ export const messages: { [messageId: string]: MessageType } = {
utilizationNoNetStats: 'Network utilization is not currently available for this VM.',
useBrowserBelow: 'Please use one of the browsers below.',
useCtrlAltEnd: 'Use Ctrl+Alt+End',
+ username: 'Username',
usingRemoteViewer: 'Using a remote viewer relies on a downloaded .vv file.',
vcpuTopology: 'VCPU Topology',
viewAllVirtualMachines: 'View All Virtual Machines',
diff --git a/src/intl/translated-messages.json b/src/intl/translated-messages.json
index 3288aa801d..ada07378a4 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.",
@@ -155,7 +154,6 @@
"yes": "Ano"
},
"de": {
- "SSHKey": "SSH-Schlüssel",
"about": "Über",
"aboutDialogDocumentationLink": "Weiter Informationen finden Sie unter {link}",
"aboutDialogDocumentationText": "Dokumentation",
@@ -607,7 +605,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 de",
"aboutDialogDocumentationLink": "Para más información, consulte {link}.",
"aboutDialogDocumentationText": "Documentación",
@@ -1059,7 +1056,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",
"aboutDialogDocumentationLink": "pour plus d'informations voir {link}",
"aboutDialogDocumentationText": "Documentation",
@@ -1598,7 +1594,6 @@
"yes": "Si"
},
"ja": {
- "SSHKey": "ユーザーの公開鍵",
"about": "バージョン情報",
"aboutDialogDocumentationLink": "詳細は {link} を参照してください",
"aboutDialogDocumentationText": "ドキュメント",
@@ -2050,7 +2045,6 @@
"youHaveNoAllowedVnicProfiles": "仮想マシンのデータセンターで vNIC プロファイルを使用するパーミッションがないので、NIC を作成または編集することはできません。"
},
"ko": {
- "SSHKey": "SSH 키 ",
"about": "정보 ",
"aboutDialogDocumentationLink": "자세한 내용은 {link}를 참조하십시오",
"aboutDialogDocumentationText": "문서",
@@ -2502,7 +2496,6 @@
"youHaveNoAllowedVnicProfiles": "가상 머신 데이터 센터에서 vNIC 프로파일을 사용할 수있는 권한이 없으므로 NIC를 만들거나 편집할 수 없습니다."
},
"pt-BR": {
- "SSHKey": "Chave SSH",
"about": "Sobre",
"aboutDialogDocumentationLink": "Para mais informações, consulte {link}",
"aboutDialogDocumentationText": "Documentação",
@@ -2954,7 +2947,6 @@
"youHaveNoAllowedVnicProfiles": "Não é possível criar ou editar NICs porque você não tem permissão para usar perfis de vNIC no data center da MV."
},
"zh-CN": {
- "SSHKey": "SSH 密钥",
"about": "关于",
"aboutDialogDocumentationLink": "更多信息 {link}",
"aboutDialogDocumentationText": "文档",
diff --git a/src/ovirtapi/index.js b/src/ovirtapi/index.js
index 132abc43ba..a3119b3903 100644
--- a/src/ovirtapi/index.js
+++ b/src/ovirtapi/index.js
@@ -71,7 +71,10 @@ const OvirtApi = {
assertLogin({ methodName: 'icon' })
return httpGet({ url: `${AppConfiguration.applicationContext}/api/icons/${id}` })
},
-
+ user ({ userId }: { userId: string }): Promise