App config and I18n selectors refactor
* store appConfig in app store on application initialization so it is easily accessible * move getEnabledLanguages from services/utils into i18n selectors * do not duplicate messages in the state - the source is components/i18n/messages.js * convert i18n components to receive current language and available languages from store using selectors Change-Id: Id6fd0da41bb75caca721ea6082603501670c36b8
This commit is contained in:
parent
6c253c999d
commit
d0a6bfddd2
20
bin/run_tests.sh
Executable file
20
bin/run_tests.sh
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright 2017 Red Hat Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
set -e
|
||||
|
||||
npm run build
|
||||
jest
|
@ -78,7 +78,8 @@
|
||||
"build": "webpack --env=prod --bail --progress",
|
||||
"build:dev": "webpack --env=dev --bail --progress",
|
||||
"start": "webpack-dev-server --env=dev --progress",
|
||||
"test": "jest",
|
||||
"test": "./bin/run_tests.sh",
|
||||
"test:quick": "jest",
|
||||
"test:watch": "jest --watchAll",
|
||||
"json2pot": "rip json2pot ./i18n/extracted-messages/**/*.json -o ./i18n/messages.pot",
|
||||
"po2json": "rip po2json -m ./i18n/extracted-messages/**/*.json",
|
||||
|
@ -14,38 +14,31 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { getEnabledLanguages } from '../services/utils';
|
||||
import { getEnabledLanguages } from '../selectors/i18n';
|
||||
import { MESSAGES } from '../components/i18n/messages';
|
||||
|
||||
export default {
|
||||
detectLanguage(messages) {
|
||||
const configLanguages = getEnabledLanguages();
|
||||
let language;
|
||||
// If the configuration contains only one language and there
|
||||
// are messages for it, return it;
|
||||
if (
|
||||
configLanguages &&
|
||||
configLanguages.length === 1 &&
|
||||
messages[configLanguages[0]]
|
||||
) {
|
||||
language = configLanguages[0];
|
||||
} else {
|
||||
const locale =
|
||||
localStorage.getItem('language') ||
|
||||
(navigator.languages && navigator.languages[0]) ||
|
||||
navigator.language ||
|
||||
navigator.userLanguage;
|
||||
// If the locale contains the country but we can't find
|
||||
// messages for it then we only use the country part:
|
||||
language = locale.match(/^[A-Za-z]+-[A-Za-z]+$/) && !messages[locale]
|
||||
? locale.split('-')[0]
|
||||
: locale;
|
||||
}
|
||||
return {
|
||||
type: 'DETECT_LANGUAGE',
|
||||
payload: {
|
||||
language,
|
||||
messages
|
||||
detectLanguage() {
|
||||
return (dispatch, getState) => {
|
||||
const languages = getEnabledLanguages(getState());
|
||||
let language;
|
||||
// If the configuration contains only one language and there
|
||||
// are messages for it, return it;
|
||||
if (languages && languages.length === 1 && MESSAGES[languages[0]]) {
|
||||
language = languages[0];
|
||||
} else {
|
||||
const locale =
|
||||
localStorage.getItem('language') ||
|
||||
(navigator.languages && navigator.languages[0]) ||
|
||||
navigator.language ||
|
||||
navigator.userLanguage;
|
||||
// If the locale contains the country but we can't find
|
||||
// messages for it then we only use the country part:
|
||||
language = locale.match(/^[A-Za-z]+-[A-Za-z]+$/) && !MESSAGES[locale]
|
||||
? locale.split('-')[0]
|
||||
: locale;
|
||||
}
|
||||
dispatch(this.chooseLanguage(language));
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -24,6 +24,7 @@ import { Redirect, Route, Switch, withRouter } from 'react-router-dom';
|
||||
import DebugScreen from './debug/DebugScreen';
|
||||
import DeploymentPlan from './deployment_plan/DeploymentPlan';
|
||||
import { getCurrentPlanName } from '../selectors/plans';
|
||||
import { getEnabledLanguages } from '../selectors/i18n';
|
||||
import Loader from './ui/Loader';
|
||||
import LoginActions from '../actions/LoginActions';
|
||||
import NavBar from './NavBar';
|
||||
@ -49,7 +50,14 @@ class AuthenticatedContent extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { currentPlanName, intl, logoutUser, plansLoaded, user } = this.props;
|
||||
const {
|
||||
currentPlanName,
|
||||
intl,
|
||||
languages,
|
||||
logoutUser,
|
||||
plansLoaded,
|
||||
user
|
||||
} = this.props;
|
||||
return (
|
||||
<Loader
|
||||
loaded={plansLoaded}
|
||||
@ -57,7 +65,11 @@ class AuthenticatedContent extends React.Component {
|
||||
global
|
||||
>
|
||||
<header>
|
||||
<NavBar user={user} onLogout={logoutUser.bind(this)} />
|
||||
<NavBar
|
||||
user={user}
|
||||
onLogout={logoutUser.bind(this)}
|
||||
languages={languages}
|
||||
/>
|
||||
</header>
|
||||
<div className="wrapper-fixed-body container-fluid">
|
||||
<div className="row">
|
||||
@ -87,6 +99,7 @@ AuthenticatedContent.propTypes = {
|
||||
fetchWorkflowExecutions: PropTypes.func,
|
||||
initializeZaqarConnection: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object,
|
||||
languages: ImmutablePropTypes.map.isRequired,
|
||||
logoutUser: PropTypes.func.isRequired,
|
||||
plansLoaded: PropTypes.bool,
|
||||
user: ImmutablePropTypes.map
|
||||
@ -102,6 +115,7 @@ const mapDispatchToProps = (dispatch, ownProps) => ({
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
languages: getEnabledLanguages(state),
|
||||
currentPlanName: getCurrentPlanName(state),
|
||||
plansLoaded: state.plans.get('plansLoaded'),
|
||||
user: state.login.getIn(['token', 'user'])
|
||||
|
@ -20,7 +20,6 @@ import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
import { getEnabledLanguages } from '../services/utils';
|
||||
import NavTab from './ui/NavTab';
|
||||
import I18nDropdown from './i18n/I18nDropdown';
|
||||
import StatusDropdown from './StatusDropdown';
|
||||
@ -57,11 +56,9 @@ export default class NavBar extends React.Component {
|
||||
}
|
||||
|
||||
_renderLanguageDropdown() {
|
||||
const languages = getEnabledLanguages();
|
||||
|
||||
// Only include the I18nDropdown if there's more than one
|
||||
// language to choose from.
|
||||
return Object.keys(languages).length > 1
|
||||
return this.props.languages.size > 1
|
||||
? <li>
|
||||
<I18nDropdown />
|
||||
</li>
|
||||
@ -135,6 +132,7 @@ export default class NavBar extends React.Component {
|
||||
}
|
||||
}
|
||||
NavBar.propTypes = {
|
||||
languages: ImmutablePropTypes.map.isRequired,
|
||||
onLogout: PropTypes.func.isRequired,
|
||||
user: ImmutablePropTypes.map
|
||||
};
|
||||
|
@ -16,15 +16,16 @@
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import Dropdown from '../ui/dropdown/Dropdown';
|
||||
import DropdownToggle from '../ui/dropdown/DropdownToggle';
|
||||
import DropdownItem from '../ui/dropdown/DropdownItem';
|
||||
import { getEnabledLanguages, getCurrentLanguage } from '../../selectors/i18n';
|
||||
import I18nActions from '../../actions/I18nActions';
|
||||
import { MESSAGES } from './messages';
|
||||
import { getEnabledLanguages } from '../../services/utils';
|
||||
|
||||
const messages = defineMessages({
|
||||
language: {
|
||||
@ -35,10 +36,10 @@ const messages = defineMessages({
|
||||
|
||||
class I18nDropdown extends React.Component {
|
||||
_renderDropdownItems() {
|
||||
const enabledLang = this.props.language;
|
||||
return getEnabledLanguages()
|
||||
const { currentLanguage, languages } = this.props;
|
||||
return languages
|
||||
.map((langName, langKey) => {
|
||||
const active = enabledLang === langKey;
|
||||
const active = currentLanguage === langKey;
|
||||
return MESSAGES[langKey] || langKey === 'en'
|
||||
? <DropdownItem
|
||||
key={`lang-${langKey}`}
|
||||
@ -66,12 +67,14 @@ class I18nDropdown extends React.Component {
|
||||
|
||||
I18nDropdown.propTypes = {
|
||||
chooseLanguage: PropTypes.func.isRequired,
|
||||
language: PropTypes.string
|
||||
currentLanguage: PropTypes.string,
|
||||
languages: ImmutablePropTypes.map.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
language: state.i18n.get('language', 'en')
|
||||
languages: getEnabledLanguages(state),
|
||||
currentLanguage: getCurrentLanguage(state)
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -22,8 +22,11 @@ import React from 'react';
|
||||
import I18nActions from '../../actions/I18nActions';
|
||||
|
||||
// NOTE(hpokorny): src/components/i18n/messages.js is generated by webpack on the fly
|
||||
import { MESSAGES, LOCALE_DATA } from './messages';
|
||||
import { getLanguage, getMessages } from '../../selectors/i18n';
|
||||
import { LOCALE_DATA } from './messages';
|
||||
import {
|
||||
getCurrentLanguage,
|
||||
getCurrentLanguageMessages
|
||||
} from '../../selectors/i18n';
|
||||
|
||||
class I18nProvider extends React.Component {
|
||||
constructor() {
|
||||
@ -32,7 +35,7 @@ class I18nProvider extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.detectLanguage(MESSAGES);
|
||||
this.props.detectLanguage();
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -63,8 +66,8 @@ const mapDispatchToProps = dispatch => {
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
language: getLanguage(state),
|
||||
messages: getMessages(state)
|
||||
language: getCurrentLanguage(state),
|
||||
messages: getCurrentLanguageMessages(state)
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -16,12 +16,12 @@
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { injectIntl } from 'react-intl';
|
||||
import { getEnabledLanguages } from '../../services/utils';
|
||||
|
||||
class LanguageInput extends React.Component {
|
||||
_renderOptions() {
|
||||
return getEnabledLanguages()
|
||||
return this.props.languages
|
||||
.map((langName, langKey) => {
|
||||
return (
|
||||
<option key={`lang-${langKey}`} value={langKey}>
|
||||
@ -63,6 +63,7 @@ LanguageInput.propTypes = {
|
||||
autoFocus: PropTypes.bool,
|
||||
chooseLanguage: PropTypes.func.isRequired,
|
||||
language: PropTypes.string,
|
||||
languages: ImmutablePropTypes.map.isRequired,
|
||||
name: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
|
@ -24,6 +24,7 @@ import ReactDOM from 'react-dom';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import FormErrorList from '../ui/forms/FormErrorList';
|
||||
import { getCurrentLanguage, getEnabledLanguages } from '../../selectors/i18n';
|
||||
import I18nActions from '../../actions/I18nActions';
|
||||
import Loader from '../ui/Loader';
|
||||
import LoginInput from '../ui/forms/LoginInput';
|
||||
@ -148,6 +149,7 @@ class Login extends React.Component {
|
||||
name="language"
|
||||
chooseLanguage={this.props.chooseLanguage}
|
||||
language={this.props.language}
|
||||
languages={this.props.languages}
|
||||
/>
|
||||
<LoginInput
|
||||
name="username"
|
||||
@ -205,6 +207,7 @@ Login.propTypes = {
|
||||
isAuthenticated: PropTypes.bool.isRequired,
|
||||
isAuthenticating: PropTypes.bool.isRequired,
|
||||
language: PropTypes.string,
|
||||
languages: ImmutablePropTypes.map.isRequired,
|
||||
location: PropTypes.object,
|
||||
userLoggedIn: PropTypes.bool.isRequired
|
||||
};
|
||||
@ -215,7 +218,8 @@ function mapStateToProps(state) {
|
||||
formFieldErrors: state.login.getIn(['loginForm', 'formFieldErrors']),
|
||||
isAuthenticated: state.login.isAuthenticated,
|
||||
isAuthenticating: state.login.get('isAuthenticating'),
|
||||
language: state.i18n.get('language', 'en'),
|
||||
language: getCurrentLanguage(state),
|
||||
languages: getEnabledLanguages(state),
|
||||
userLoggedIn: state.login.hasIn(['keystoneAccess', 'user'])
|
||||
};
|
||||
}
|
||||
|
24
src/js/immutableRecords/appConfig.js
Normal file
24
src/js/immutableRecords/appConfig.js
Normal file
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright 2017 Red Hat Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { List, Record } from 'immutable';
|
||||
|
||||
export const AppConfig = Record({
|
||||
zaqarDefaultQueue: 'tripleo',
|
||||
zaqarLoggerQueue: 'tripleo-ui-logging',
|
||||
excludedLanguages: List(),
|
||||
loggers: List(['console', 'zaqar'])
|
||||
});
|
26
src/js/reducers/appConfig.js
Normal file
26
src/js/reducers/appConfig.js
Normal file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright 2017 Red Hat Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { AppConfig } from '../immutableRecords/appConfig';
|
||||
|
||||
const initialState = new AppConfig();
|
||||
|
||||
export default function appConfig(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import { reducer as formReducer } from 'redux-form';
|
||||
|
||||
import appConfig from './appConfig';
|
||||
import environmentConfigurationReducer from './environmentConfigurationReducer';
|
||||
import filtersReducer from './filtersReducer';
|
||||
import i18nReducer from './i18nReducer';
|
||||
@ -33,6 +34,7 @@ import validationsReducer from './validationsReducer';
|
||||
import workflowExecutionsReducer from './workflowExecutionsReducer';
|
||||
|
||||
const appReducer = combineReducers({
|
||||
appConfig,
|
||||
environmentConfiguration: environmentConfigurationReducer,
|
||||
executions: workflowExecutionsReducer,
|
||||
filters: filtersReducer,
|
||||
|
@ -14,20 +14,14 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { Map } from 'immutable';
|
||||
import { Record } from 'immutable';
|
||||
|
||||
const initialState = Map({
|
||||
language: 'en',
|
||||
messages: {}
|
||||
const InitialState = Record({
|
||||
language: 'en'
|
||||
});
|
||||
|
||||
export default function i18nReducer(state = initialState, action) {
|
||||
export default function i18nReducer(state = new InitialState(), action) {
|
||||
switch (action.type) {
|
||||
case 'DETECT_LANGUAGE':
|
||||
return state
|
||||
.set('language', action.payload.language)
|
||||
.set('messages', action.payload.messages);
|
||||
|
||||
case 'CHOOSE_LANGUAGE':
|
||||
return state.set('language', action.payload);
|
||||
|
||||
|
@ -16,29 +16,40 @@
|
||||
|
||||
import { createSelector } from 'reselect';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { Map } from 'immutable';
|
||||
|
||||
const languageSelector = state => state.i18n.get('language');
|
||||
import { LANGUAGE_NAMES } from '../constants/i18n';
|
||||
// TODO(jtomasek): This should rather be in /constants
|
||||
import { MESSAGES } from '../components/i18n/messages';
|
||||
|
||||
const messagesSelector = state => state.i18n.get('messages');
|
||||
const getMessages = () => MESSAGES;
|
||||
const getAvailableLanguages = () => LANGUAGE_NAMES;
|
||||
export const getAppConfig = state => state.appConfig;
|
||||
export const getCurrentLanguage = state => state.i18n.language;
|
||||
|
||||
export const getLanguage = createSelector(
|
||||
[languageSelector],
|
||||
language => language
|
||||
);
|
||||
|
||||
export const getMessages = createSelector(
|
||||
[languageSelector, messagesSelector],
|
||||
export const getCurrentLanguageMessages = createSelector(
|
||||
[getCurrentLanguage, getMessages],
|
||||
(language, messages) => messages[language]
|
||||
);
|
||||
|
||||
export const getIntl = createSelector(
|
||||
[languageSelector, messagesSelector],
|
||||
[getCurrentLanguage, getCurrentLanguageMessages],
|
||||
(language, messages) => {
|
||||
const intlProvider = new IntlProvider(
|
||||
{ locale: language, messages: messages[language] },
|
||||
{ locale: language, messages: messages },
|
||||
{}
|
||||
);
|
||||
const { intl } = intlProvider.getChildContext();
|
||||
return intl;
|
||||
}
|
||||
);
|
||||
|
||||
export const getEnabledLanguages = createSelector(
|
||||
[getAppConfig, getAvailableLanguages],
|
||||
(appConfig, languages) =>
|
||||
// with immutablejs v 4.0.0 this can be replaced with
|
||||
// Map(languages).deleteAll(appConfig.excludedLanguages).sort();
|
||||
Map(languages)
|
||||
.filterNot((language, key) => appConfig.excludedLanguages.includes(key))
|
||||
.sort()
|
||||
);
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
import { Map, List } from 'immutable';
|
||||
import store from '../store';
|
||||
import { LANGUAGE_NAMES } from '../constants/i18n';
|
||||
|
||||
/**
|
||||
* Returns the public url of an openstack API,
|
||||
@ -62,13 +61,3 @@ export function getProjectId() {
|
||||
export function getAppConfig() {
|
||||
return window.tripleOUiConfig || {};
|
||||
}
|
||||
|
||||
export function getEnabledLanguages() {
|
||||
const excludedLanguages = getAppConfig().excludedLanguages || [];
|
||||
let configLanguages = Object.assign({}, LANGUAGE_NAMES);
|
||||
excludedLanguages.map(language => {
|
||||
delete configLanguages[language];
|
||||
});
|
||||
|
||||
return Map(configLanguages).sort();
|
||||
}
|
||||
|
@ -16,10 +16,12 @@
|
||||
|
||||
import { applyMiddleware, createStore } from 'redux';
|
||||
import cookie from 'react-cookie';
|
||||
import { fromJS } from 'immutable';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
import createLogger from 'redux-logger';
|
||||
import logger, { predicate } from './services/logging/LoggingService';
|
||||
|
||||
import { AppConfig } from './immutableRecords/appConfig';
|
||||
import appReducer from './reducers/appReducer';
|
||||
import { InitialPlanState } from './immutableRecords/plans';
|
||||
import { InitialLoginState } from './immutableRecords/login';
|
||||
@ -27,6 +29,7 @@ import { getIntl } from './selectors/i18n';
|
||||
|
||||
const hydrateStore = () => {
|
||||
return {
|
||||
appConfig: new AppConfig(window && fromJS(window.tripleOUiConfig)),
|
||||
plans: new InitialPlanState({
|
||||
currentPlanName: getStoredPlanName()
|
||||
}),
|
||||
|
Loading…
Reference in New Issue
Block a user