Skip to content

Commit

Permalink
Merge pull request #1372 from rszwajko/userLocale
Browse files Browse the repository at this point in the history
Change locale and display email via Account Settings
  • Loading branch information
sgratch authored Mar 10, 2021
2 parents 4c570ae + 7bf1930 commit 6e1f4c9
Show file tree
Hide file tree
Showing 17 changed files with 394 additions and 118 deletions.
58 changes: 28 additions & 30 deletions src/actions/options.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,13 @@
// @flow

import type { UserOptionsType, SshKeyType } from '_/ovirtapi/types'
import type { RemoteUserOptionsType, 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'
import * as C from '_/constants'

export function setConsoleOptions ({ vmId, options }: Object): Object {
return {
type: SET_CONSOLE_OPTIONS,
type: C.SET_CONSOLE_OPTIONS,
payload: {
vmId,
options,
Expand All @@ -30,7 +17,7 @@ export function setConsoleOptions ({ vmId, options }: Object): Object {

export function getConsoleOptions ({ vmId }: Object): Object {
return {
type: GET_CONSOLE_OPTIONS,
type: C.GET_CONSOLE_OPTIONS,
payload: {
vmId,
},
Expand All @@ -39,7 +26,7 @@ export function getConsoleOptions ({ vmId }: Object): Object {

export function saveConsoleOptions ({ vmId, options }: Object): Object {
return {
type: SAVE_CONSOLE_OPTIONS,
type: C.SAVE_CONSOLE_OPTIONS,
payload: {
vmId,
options,
Expand All @@ -49,7 +36,7 @@ export function saveConsoleOptions ({ vmId, options }: Object): Object {

export function getSSHKey ({ userId }: Object): Object {
return {
type: GET_SSH_KEY,
type: C.GET_SSH_KEY,
payload: {
userId,
},
Expand All @@ -58,7 +45,7 @@ export function getSSHKey ({ userId }: Object): Object {

export function setSSHKey ({ key, id }: SshKeyType): Object {
return {
type: SET_SSH_KEY,
type: C.SET_SSH_KEY,
payload: {
key,
id,
Expand All @@ -68,17 +55,17 @@ export function setSSHKey ({ key, id }: SshKeyType): Object {

export function setOption ({ key, value }: Object): Object {
return {
type: SET_OPTION,
type: C.SET_OPTION,
payload: {
key,
value,
},
}
}

export function loadUserOptions (userOptions: UserOptionsType): LoadUserOptionsActionType {
export function loadUserOptions (userOptions: RemoteUserOptionsType): LoadUserOptionsActionType {
return {
type: LOAD_USER_OPTIONS,
type: C.LOAD_USER_OPTIONS,
payload: {
userOptions,
},
Expand All @@ -87,19 +74,19 @@ export function loadUserOptions (userOptions: UserOptionsType): LoadUserOptionsA

export function loadingUserOptionsInProgress (): Object {
return {
type: LOAD_USER_OPTIONS_IN_PROGRESS,
type: C.LOAD_USER_OPTIONS_IN_PROGRESS,
}
}

export function loadingUserOptionsFinished (): Object {
return {
type: LOAD_USER_OPTIONS_FINISHED,
type: C.LOAD_USER_OPTIONS_FINISHED,
}
}

export function saveGlobalOptions ({ values: { sshKey, language, showNotifications, notificationSnoozeDuration, updateRate } = {} }: Object, { transactionId }: Object): SaveGlobalOptionsActionType {
return {
type: SAVE_GLOBAL_OPTIONS,
type: C.SAVE_GLOBAL_OPTIONS,
payload: {
sshKey,
language,
Expand All @@ -115,7 +102,7 @@ export function saveGlobalOptions ({ values: { sshKey, language, showNotificatio

export function saveSSHKey ({ key, userId, sshId }: Object): Object {
return {
type: SAVE_SSH_KEY,
type: C.SAVE_SSH_KEY,
payload: {
key,
userId,
Expand All @@ -124,11 +111,22 @@ export function saveSSHKey ({ key, userId, sshId }: Object): Object {
}
}

export function persistUserOptions ({ options, userId }: Object): Object {
export function persistUserOption ({ userId, name, content, optionId }: Object): Object {
return {
type: PERSIST_OPTIONS,
type: C.PERSIST_OPTION,
payload: {
userId,
name,
content,
optionId,
},
}
}

export function fetchUserOptions ({ userId }: Object): Object {
return {
type: C.FETCH_OPTIONS,
payload: {
options,
userId,
},
}
Expand Down
4 changes: 2 additions & 2 deletions src/actions/types.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// @flow
import * as C from '_/constants'
import type { UserOptionsType } from '_/ovirtapi/types'
import type { RemoteUserOptionsType } from '_/ovirtapi/types'

export type LoadUserOptionsActionType = {
type: C.LOAD_USER_OPTIONS,
payload: {
userOptions: UserOptionsType
userOptions: RemoteUserOptionsType
}
}

Expand Down
26 changes: 22 additions & 4 deletions src/components/UserSettings/GlobalSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { push } from 'connected-react-router'
import { saveGlobalOptions } from '_/actions'
import { FormControl, Switch } from 'patternfly-react'
import { msg } from '_/intl'
import localeWithFullName from '_/intl/localeWithFullName'
import style from './style.css'

import { Settings, SettingsBase } from '../Settings'
Expand Down Expand Up @@ -143,6 +144,23 @@ class GlobalSettings extends Component {
title: msg.username(),
body: <span>{config.userName}</span>,
},
{
title: msg.email(),
body: <span>{config.email}</span>,
},
{
title: translatedLabels.language,
body: (
<div className={style['half-width']}>
<SelectBox
id={`${idPrefix}-language`}
items={Object.entries(localeWithFullName).map(([id, value]) => ({ id, value }))}
selected={draftValues.language}
onChange={onChange('language')}
/>
</div>
),
},
{
title: translatedLabels.sshKey,
tooltip: msg.sshKeyTooltip(),
Expand Down Expand Up @@ -236,10 +254,10 @@ export default connect(
},
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']),
language: options.getIn(['remoteOptions', 'locale', 'content']),
showNotifications: options.getIn(['localOptions', 'showNotifications']),
notificationSnoozeDuration: options.getIn(['localOptions', 'notificationSnoozeDuration']),
updateRate: options.getIn(['remoteOptions', 'updateRate', 'content']),
},
lastTransactionId: options.getIn(['lastTransactions', 'global', 'transactionId'], ''),
}),
Expand Down
12 changes: 2 additions & 10 deletions src/config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import $ from 'jquery'

import { getURLQueryParameterByName } from '_/helpers'
import { setLogDebug } from './logger'
import { localeFromUrl } from '_/intl'

const CONFIG_URL = '/ovirt-engine/web-ui/ovirt-web-ui.config'

Expand All @@ -18,13 +18,11 @@ const AppConfiguration = {
cockpitPort: '9090',

queryParams: { // from URL
locale: null,
locale: localeFromUrl,
},
}

export function readConfiguration () {
parseQueryParams()

return new Promise((resolve, reject) => {
$.ajax({
url: CONFIG_URL,
Expand All @@ -43,10 +41,4 @@ export function readConfiguration () {
})
}

function parseQueryParams () {
// TODO: align this with intl/index.js:getLocaleFromUrl()
AppConfiguration.queryParams.locale = getURLQueryParameterByName('locale')
console.log('parseQueryParams, provided locale: ', AppConfiguration.queryParams.locale)
}

export default AppConfiguration
3 changes: 2 additions & 1 deletion src/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const EDIT_VM = 'EDIT_VM'
export const EDIT_VM_DISK = 'EDIT_VM_DISK'
export const EDIT_VM_NIC = 'EDIT_VM_NIC'
export const FAILED_EXTERNAL_ACTION = 'FAILED_EXTERNAL_ACTION'
export const FETCH_OPTIONS = 'FETCH_OPTIONS'
export const GET_ALL_CLUSTERS = 'GET_ALL_CLUSTERS'
export const GET_ALL_EVENTS = 'GET_ALL_EVENTS'
export const GET_ALL_HOSTS = 'GET_ALL_HOSTS'
Expand Down Expand Up @@ -59,7 +60,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 PERSIST_OPTION = 'PERSIST_OPTION'
export const POOL_ACTION_IN_PROGRESS = 'POOL_ACTION_IN_PROGRESS'
export const REDIRECT = 'REDIRECT'
export const REFRESH_DATA = 'REFRESH_DATA'
Expand Down
5 changes: 4 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import AppConfiguration, { readConfiguration } from '_/config'
import { login } from '_/actions'

import App from './App'
import LocaleReloader from './intl/LocaleReloader'
import GlobalErrorBoundary from './GlobalErrorBoundary'

// Patternfly dependencies
Expand All @@ -34,7 +35,9 @@ function renderApp (store: Object, errorBridge: Object) {
ReactDOM.render(
<GlobalErrorBoundary errorBridge={errorBridge} store={store}>
<Provider store={store}>
<App history={store.history} />
<LocaleReloader>
<App history={store.history} />
</LocaleReloader>
</Provider>
</GlobalErrorBoundary>,

Expand Down
46 changes: 46 additions & 0 deletions src/intl/LocaleReloader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useEffect } from 'react'
import PropTypes from 'prop-types'

import { connect } from 'react-redux'

import { locale as mergedLocale } from '_/intl'

const LocaleReloader = ({ children, localeFromStore, loadingFinished }) => {
/* Basic flow:
* 1. after language settings change the value is saved on the server, in the Redux store and local store
* 2. value from the Redux store is passed via props and is used to trigger useEffect hook
* 3. in the hook the change is detected by comparing static resources with Redux store
* 4. if detected, the locales change requires (currently) page reload to regenerate static i18n resources i.e. global "msg" object
* 5. after the reload both Redux store and custom i18n resources use the same locales
* 6. static resources are used directly i.e. by calling msg.someLabel() in the components
* Special cases:
* 1. locale provided by URL (<server>/?locale=en_US) and cookie is used only if there is no locale in the local storage (which
* indicates that there is no locales persisted on the server).
* 2. updating the language while still on user setting page - the reload should wait until user sees success confirmation.
* If the reload is triggered too early, the browser will display a warning dialog "are you sure you want to reload,
* you changes will be lost". Ready for reload state is detected by watching loadingFinished flag.
* 3. fist run(no data in local storage) - UI will try to gues user locale. When user settings will be fetched
* from the server and incorrect locale was guessed then an additional reload will happen.
* 4. no property on server exists but UI was launched using non-default locale(i.e. from local storage or URL) - save that locale on the server.
*/
useEffect(() => {
if (loadingFinished && localeFromStore !== mergedLocale) {
console.warn(`reload due to locale change: ${mergedLocale} -> ${localeFromStore}`)
window.location.reload()
}
}, [localeFromStore, loadingFinished])
return ([ children ])
}

LocaleReloader.propTypes = {
children: PropTypes.node,
localeFromStore: PropTypes.string.isRequired,
loadingFinished: PropTypes.bool.isRequired,
}

export default connect(
state => ({
localeFromStore: state.options.getIn(['remoteOptions', 'locale', 'content']),
loadingFinished: state.options.getIn(['loadingFinished'], false),
})
)(LocaleReloader)
3 changes: 2 additions & 1 deletion src/intl/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @flow

import IntlMessageFormat from 'intl-messageformat'
import { initIntl } from './initialize'
import { initIntl, getLocaleFromUrl } from './initialize'

import { messages, type MessageIdType, type MessageType } from './messages'
import translatedMessages from './translated-messages.json'
Expand All @@ -16,6 +16,7 @@ export const BASE_LOCALE_SET: Set<string> = new Set(Object.keys(localeWithFullNa
* Currently selected locale
*/
export const locale: string = initIntl()
export const localeFromUrl: ?string = getLocaleFromUrl()

function getMessage (id: MessageIdType): string {
const message = getMessageForLocale(id, locale)
Expand Down
17 changes: 15 additions & 2 deletions src/intl/index.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
/* eslint-env jest */

import { enumMsg } from './index'
import { enumMsg, BASE_LOCALE_SET, DEFAULT_LOCALE } from './index'
import localeWithFullName from './localeWithFullName.json'

describe('intl', () => {
it('enumMsg should survive unknown enum item', () => {
const unknownEnumItem = 'unknownEnumItem'
expect(enumMsg('UnknownEnum', unknownEnumItem)).toEqual(unknownEnumItem)
})

it('default locale exists in supported locales', () => {
expect(BASE_LOCALE_SET.has(DEFAULT_LOCALE)).toBeTruthy()
})

it('each translated locale (full name) should exist in the supported locales', () => {
Object.keys(localeWithFullName).forEach(id =>
expect(BASE_LOCALE_SET.has(id)).toBeTruthy())
})

it('each supported locale should be translated', () => {
BASE_LOCALE_SET.forEach(id => expect(localeWithFullName[id]).toBeTruthy())
})
})
11 changes: 9 additions & 2 deletions src/intl/initialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import 'moment-duration-format'

import { BASE_LOCALE_SET, DEFAULT_LOCALE, DUMMY_LOCALE } from './index'
import { timeDurations } from './time-durations'
import { loadFromLocalStorage } from '_/storage'
import type { RemoteUserOptionsType } from '_/ovirtapi/types'

export function initIntl (forceLocale: ?string): string {
const locale: string = forceLocale || discoverUserLocale()
Expand All @@ -25,10 +27,15 @@ export function initIntl (forceLocale: ?string): string {
}

function discoverUserLocale (): string {
return getLocaleFromUrl() || getBrowserLocale() || DEFAULT_LOCALE
return loadLocaleFromLocalStorage() || getLocaleFromUrl() || getBrowserLocale() || DEFAULT_LOCALE
}

function getLocaleFromUrl (): ?string {
function loadLocaleFromLocalStorage (): ?string {
const { remoteOptions: { locale: { content } = {} } = {} } : {remoteOptions: RemoteUserOptionsType} = JSON.parse(loadFromLocalStorage('options')) || {}
return content
}

export function getLocaleFromUrl (): ?string {
const localeMatch = /locale=(\w{2}([-_]\w{2})?)/.exec(window.location.search)
if (localeMatch === null) {
return null
Expand Down
Loading

0 comments on commit 6e1f4c9

Please sign in to comment.