Add i18n to Notifications

Adding i18n to the Notifications is a little different to other
components because the title and message are defined outside the
component, often coming from external web service calls.

This patch adds an extra thunk parameter which can be used inside
actions to translate strings.

Change-Id: I6335c6ccb92e3077ba6400fa6364446a757863eb
This commit is contained in:
Florian Fuchs 2017-01-12 14:40:54 +01:00 committed by Julie Pichon
parent a630aed9cb
commit add59febaf
14 changed files with 174 additions and 42 deletions

View File

@ -4,6 +4,7 @@ import * as utils from '../../js/services/utils';
import EnvironmentConfigurationActions from '../../js/actions/EnvironmentConfigurationActions';
import { browserHistory } from 'react-router';
import MistralApiService from '../../js/services/MistralApiService';
import { mockGetIntl } from './utils';
// Use this to mock asynchronous functions which return a promise.
// The promise will immediately resolve with `data`.
@ -56,7 +57,7 @@ describe('EnvironmentConfigurationActions', () => {
\"environments/network-isolation.yaml\"}]}}`
}));
EnvironmentConfigurationActions.updateEnvironmentConfiguration(
'overcloud', {}, {}, '/redirect/url')(() => {}, () => {}
'overcloud', {}, {}, '/redirect/url')(() => {}, () => {}, mockGetIntl
);
// Call done with a minimal timeout.
setTimeout(() => { done(); }, 1);

View File

@ -2,6 +2,7 @@ import when from 'when';
import IronicApiService from '../../js/services/IronicApiService';
import MistralApiService from '../../js/services/MistralApiService';
import { mockGetIntl } from './utils';
import NodesActions from '../../js/actions/NodesActions';
import NotificationActions from '../../js/actions/NotificationActions';
import NodesConstants from '../../js/constants/NodesConstants';
@ -196,7 +197,7 @@ describe('nodesIntrospectionFinished', () => {
ttl: 3600
};
NodesActions.nodesIntrospectionFinished(messagePayload)(() => {}, () => {});
NodesActions.nodesIntrospectionFinished(messagePayload)(() => {}, () => {}, mockGetIntl);
expect(NodesActions.fetchNodes).toHaveBeenCalled();
expect(NotificationActions.notify).toHaveBeenCalled();
@ -217,7 +218,7 @@ describe('nodesIntrospectionFinished', () => {
ttl: 3600
};
NodesActions.nodesIntrospectionFinished(messagePayload)(() => {}, () => {});
NodesActions.nodesIntrospectionFinished(messagePayload)(() => {}, () => {}, mockGetIntl);
expect(NodesActions.fetchNodes).toHaveBeenCalled();
expect(NotificationActions.notify).toHaveBeenCalledWith(

View File

@ -6,6 +6,7 @@ import ParametersActions from '../../js/actions/ParametersActions';
import ParametersConstants from '../../js/constants/ParametersConstants';
import MistralApiService from '../../js/services/MistralApiService';
import MistralConstants from '../../js/constants/MistralConstants';
import { mockGetIntl } from './utils';
import { normalizeParameters } from '../../js/actions/ParametersActions';
@ -65,7 +66,7 @@ describe('ParametersActions', () => {
.and.callFake(createResolvingPromise(responseBody));
// Call the action creator and the resulting action.
// In this case, dispatch and getState are just empty placeHolders.
ParametersActions.fetchParameters('overcloud')(() => {}, () => {});
ParametersActions.fetchParameters('overcloud')(() => {}, () => {}, mockGetIntl);
// Call done with a minimal timeout.
setTimeout(() => { done(); }, 1);
});
@ -138,7 +139,9 @@ describe('ParametersActions', () => {
.and.callFake(createRejectingPromise(error));
// Call the action creator and the resulting action.
// In this case, dispatch and getState are just empty placeHolders.
ParametersActions.updateParameters('overcloud', { foo: 'bar' })(() => {}, () => {});
ParametersActions.updateParameters('overcloud', { foo: 'bar' })(
() => {}, () => {}, mockGetIntl
);
// Call done with a minimal timeout.
setTimeout(() => { done(); }, 1);
});

View File

@ -2,6 +2,7 @@ import when from 'when';
import * as utils from '../../js/services/utils';
import MistralApiService from '../../js/services/MistralApiService';
import { mockGetIntl } from './utils';
import PlansActions from '../../js/actions/PlansActions';
import SwiftApiService from '../../js/services/SwiftApiService';
@ -28,7 +29,7 @@ describe('PlansActions', () => {
spyOn(PlansActions, '_uploadFilesToContainer').and.callFake(createResolvingPromise());
// Call the action creator and the resulting action.
// In this case, dispatch and getState are just empty placeHolders.
PlansActions.updatePlan('somecloud', {})(() => {}, () => {});
PlansActions.updatePlan('somecloud', {})(() => {}, () => {}, mockGetIntl);
// Call done with a minimal timeout.
setTimeout(() => { done(); }, 1);
});
@ -57,7 +58,7 @@ describe('PlansActions', () => {
.and.callFake(createResolvingPromise({ state: 'SUCCESS' }));
// Call the action creator and the resulting action.
// In this case, dispatch and getState are just empty placeHolders.
PlansActions.createPlan('somecloud', {})(() => {}, () => {});
PlansActions.createPlan('somecloud', {})(() => {}, () => {}, mockGetIntl);
// Call done with a minimal timeout.
setTimeout(() => { done(); }, 1);
});
@ -76,7 +77,7 @@ describe('PlansActions', () => {
spyOn(MistralApiService, 'runAction').and.callFake(createResolvingPromise());
// Call the action creator and the resulting action.
// In this case, dispatch and getState are just empty placeHolders.
PlansActions.deletePlan('somecloud')(() => {}, () => {});
PlansActions.deletePlan('somecloud')(() => {}, () => {}, mockGetIntl);
// Call done with a minimal timeout.
setTimeout(() => { done(); }, 1);
});

View File

@ -3,6 +3,7 @@ import when from 'when';
import { Map } from 'immutable';
import MistralApiService from '../../js/services/MistralApiService';
import { mockGetIntl } from './utils';
import RegisterNodesActions from '../../js/actions/RegisterNodesActions';
import NodesActions from '../../js/actions/NodesActions';
import NotificationActions from '../../js/actions/NotificationActions';
@ -76,7 +77,7 @@ describe('nodesRegistrationFinished', () => {
const successNotification = {
type: 'success',
title: 'Nodes Registration Complete',
message: 'The nodes were successfully registered'
message: 'The nodes were successfully registered.'
};
spyOn(NotificationActions, 'notify');
@ -89,7 +90,7 @@ describe('nodesRegistrationFinished', () => {
currentPlanName: 'testplan'
})
};
});
}, mockGetIntl);
expect(NodesActions.addNodes).toHaveBeenCalledWith(normalizedRegisteredNodes);
expect(NodesActions.fetchNodes).toHaveBeenCalledWith();
@ -131,7 +132,7 @@ describe('nodesRegistrationFinished', () => {
currentPlanName: 'testplan'
})
};
});
}, mockGetIntl);
expect(browserHistory.push).toHaveBeenCalledWith('/nodes/registered/register');
expect(NodesActions.addNodes).toHaveBeenCalledWith(normalizedRegisteredNodes);

View File

@ -0,0 +1,7 @@
export const mockGetIntl = {
getIntl: () => {
return {
formatMessage: msgObj => msgObj.defaultMessage
};
}
};

View File

@ -1,7 +1,20 @@
import { defineMessages } from 'react-intl';
import NotificationActions from '../actions/NotificationActions';
import PlansConstants from '../constants/PlansConstants';
import ValidationsActions from '../actions/ValidationsActions';
const messages = defineMessages({
planActivatedNotificationTitle: {
id: 'CurrentPlanActions.planActivatedNotificationTitle',
defaultMessage: 'Plan Activated'
},
planActivatedNotificationMessage: {
id: 'CurrentPlanActions.planActivatedNotificationMessage',
defaultMessage: 'The plan {planName} was activated.'
}
});
export default {
detectPlan() {
return (dispatch, getState) => {
@ -46,10 +59,11 @@ export default {
},
choosePlan(planName) {
return dispatch => {
return (dispatch, getState, { getIntl }) => {
const { formatMessage } = getIntl(getState());
dispatch(NotificationActions.notify({
title: 'Plan Activated',
message: 'The plan ' + planName + ' was activated.',
title: formatMessage(messages.planActivatedNotificationTitle),
message: formatMessage(messages.planActivatedNotificationMessage, { planName: planName }),
type: 'success'
}));
storePlan(planName);

View File

@ -1,3 +1,4 @@
import { defineMessages } from 'react-intl';
import { normalize, arrayOf } from 'normalizr';
import yaml from 'js-yaml';
@ -12,6 +13,17 @@ import logger from '../services/logger';
import SwiftApiErrorHandler from '../services/SwiftApiErrorHandler';
import SwiftApiService from '../services/SwiftApiService';
const messages = defineMessages({
envConfigUpdatedNotificationMessage: {
id: 'EnvironmentConfigurationActions.envConfigUpdatedNotificationMessage',
defaultMessage: 'The Environment Configuration has been successfully updated.'
},
envConfigUpdatedNotificationTitle: {
id: 'EnvironmentConfigurationActions.envConfigUpdatedNotificationTitle',
defaultMessage: 'Environment Configuration updated'
}
});
export default {
fetchEnvironmentConfiguration(planName, redirectPath) {
@ -55,7 +67,8 @@ export default {
},
updateEnvironmentConfiguration(planName, data, formFields, redirectPath) {
return dispatch => {
return (dispatch, getState, { getIntl }) => {
const { formatMessage } = getIntl(getState());
dispatch(this.updateEnvironmentConfigurationPending());
MistralApiService.runAction(MistralConstants.CAPABILITIES_UPDATE,
{ environments: data, container: planName })
@ -64,8 +77,8 @@ export default {
dispatch(this.updateEnvironmentConfigurationSuccess(enabledEnvs));
if (redirectPath) { browserHistory.push(redirectPath); }
dispatch(NotificationActions.notify({
title: 'Environment Configuration updated',
message: 'The Environment Configuration has been successfully updated',
title: formatMessage(messages.envConfigUpdatedNotificationTitle),
message: formatMessage(messages.envConfigUpdatedNotificationMessage),
type: 'success'
}));
}).catch((error) => {

View File

@ -1,3 +1,4 @@
import { defineMessages } from 'react-intl';
import { normalize, arrayOf } from 'normalizr';
import when from 'when';
@ -13,6 +14,21 @@ import MistralConstants from '../constants/MistralConstants';
import logger from '../services/logger';
import { setNodeCapability } from '../utils/nodes';
const messages = defineMessages({
introspectionNotificationMessage: {
id: 'NodesActions.introspectionNotificationMessage',
defaultMessage: 'Selected nodes were successfully introspected.'
},
introspectionNotificationTitle: {
id: 'NodesActions.introspectionNotificationTitle',
defaultMessage: 'Nodes Introspection Complete'
},
introspectionFailedNotificationTitle: {
id: 'NodesActions.introspectionFailedNotificationTitle',
defaultMessage: 'Nodes Introspection Failed'
}
});
export default {
startOperation(nodeIds) {
return {
@ -113,7 +129,8 @@ export default {
},
nodesIntrospectionFinished(messagePayload) {
return (dispatch, getState) => {
return (dispatch, getState, { getIntl }) => {
const { formatMessage } = getIntl(getState());
const nodeIds = messagePayload.execution.input.node_uuids;
dispatch(this.finishOperation(nodeIds));
dispatch(this.fetchNodes());
@ -122,15 +139,15 @@ export default {
case 'SUCCESS': {
dispatch(NotificationActions.notify({
type: 'success',
title: 'Nodes Introspection Complete',
message: 'Selected nodes were successfully introspected'
title: formatMessage(messages.introspectionNotificationTitle),
message: formatMessage(messages.introspectionNotificationMessage)
}));
break;
}
case 'FAILED': {
dispatch(NotificationActions.notify({
type: 'error',
title: 'Nodes Introspection Failed',
title: formatMessage(messages.introspectionFailedNotificationTitle),
message: messagePayload.message.join(', ')
}));
break;

View File

@ -1,3 +1,4 @@
import { defineMessages } from 'react-intl';
import { normalize } from 'normalizr';
import { browserHistory } from 'react-router';
import uuid from 'node-uuid';
@ -11,6 +12,17 @@ import MistralConstants from '../constants/MistralConstants';
import logger from '../services/logger';
import { resourceGroupSchema } from '../normalizrSchemas/parameters';
const messages = defineMessages({
parametersUpdatedNotficationTitle: {
id: 'ParametersActions.parametersUpdatedNotficationTitle',
defaultMessage: 'Parameters updated'
},
parametersUpdatedNotficationMessage: {
id: 'ParametersActions.parametersUpdatedNotficationMessage',
defaultMessage: 'The Deployment parameters have been successfully updated.'
}
});
export default {
fetchParametersPending() {
return {
@ -76,15 +88,16 @@ export default {
},
updateParameters(planName, data, inputFieldNames, url) {
return dispatch => {
return (dispatch, getState, { getIntl }) => {
const { formatMessage } = getIntl(getState());
dispatch(this.updateParametersPending());
MistralApiService.runAction(MistralConstants.PARAMETERS_UPDATE,
{ container: planName, parameters: data })
.then(response => {
dispatch(this.updateParametersSuccess(data));
dispatch(NotificationActions.notify({
title: 'Parameters updated',
message: 'The Deployment parameters have been successfully updated',
title: formatMessage(messages.parametersUpdatedNotficationTitle),
message: formatMessage(messages.parametersUpdatedNotficationMessage),
type: 'success'
}));
if (url) { browserHistory.push(url); }

View File

@ -1,3 +1,4 @@
import { defineMessages } from 'react-intl';
import { fromJS } from 'immutable';
import { normalize, arrayOf } from 'normalizr';
import when from 'when';
@ -15,6 +16,37 @@ import SwiftApiErrorHandler from '../services/SwiftApiErrorHandler';
import SwiftApiService from '../services/SwiftApiService';
import MistralConstants from '../constants/MistralConstants';
const messages = defineMessages({
planCreatedNotificationTitle: {
id: 'PlansActions.planCreatedNotificationTitle',
defaultMessage: 'Plan was created'
},
planCreatedNotificationMessage: {
id: 'PlansActions.planCreatedNotificationMessage',
defaultMessage: 'The plan {planName} was successfully created.'
},
planUpdatedNotificationTitle: {
id: 'PlansActions.planUpdatedNotificationTitle',
defaultMessage: 'Plan Updated'
},
planUpdatedNotificationMessage: {
id: 'PlansActions.planUpdatedNotificationMessage',
defaultMessage: 'The plan {planName} was successfully updated.'
},
planDeletedNotificationTitle: {
id: 'PlansActions.planDeletedNotificationTitle',
defaultMessage: 'Plan Deleted'
},
planDeletedNotificationMessage: {
id: 'PlansActions.planDeletedNotificationMessage',
defaultMessage: 'The plan {planName} was successfully deleted.'
},
deploymentFailedNotificationTitle: {
id: 'PlansActions.deploymentFailedNotificationTitle',
defaultMessage: 'Deployment Failed'
}
});
export default {
requestPlans() {
return {
@ -100,14 +132,15 @@ export default {
},
updatePlan(planName, planFiles) {
return dispatch => {
return (dispatch, getState, { getIntl }) => {
const { formatMessage } = getIntl(getState());
dispatch(this.updatePlanPending(planName));
this._uploadFilesToContainer(planName, fromJS(planFiles), dispatch).then(() => {
dispatch(this.updatePlanSuccess(planName));
browserHistory.push('/plans/list');
dispatch(NotificationActions.notify({
title: 'Plan Updated',
message: `The plan ${planName} was successfully updated.`,
title: formatMessage(messages.planUpdatedNotificationTitle),
message: formatMessage(messages.planUpdatedNotificationMessage, { planName: planName }),
type: 'success'
}));
dispatch(this.fetchPlans());
@ -122,14 +155,15 @@ export default {
},
updatePlanFromTarball(planName, file) {
return (dispatch) => {
return (dispatch, getState, { getIntl }) => {
const { formatMessage } = getIntl(getState());
dispatch(this.updatePlanPending(planName));
SwiftApiService.uploadTarball(planName, file).then((response) => {
dispatch(this.updatePlanSuccess(planName));
browserHistory.push('/plans/list');
dispatch(NotificationActions.notify({
title: 'Plan Updated',
message: `The plan ${planName} was successfully updated.`,
title: formatMessage(messages.planUpdatedNotificationTitle),
message: formatMessage(messages.planUpdatedNotificationMessage, { planName: planName }),
type: 'success'
}));
dispatch(this.fetchPlans());
@ -249,13 +283,15 @@ export default {
},
createPlanFinished(payload) {
return (dispatch) => {
return (dispatch, getState, { getIntl }) => {
const { formatMessage } = getIntl(getState());
if(payload.status === 'SUCCESS') {
const planName = payload.execution.input.container;
dispatch(this.createPlanSuccess());
dispatch(NotificationActions.notify({
type: 'success',
title: 'Plan was created',
message: `The plan ${payload.execution.input.container} was successfully created`
title: formatMessage(messages.planCreatedNotificationTitle),
message: formatMessage(messages.planCreatedNotificationMessage, { planName: planName })
}));
dispatch(this.fetchPlans());
browserHistory.push('/plans/list');
@ -329,15 +365,16 @@ export default {
},
deletePlan(planName) {
return dispatch => {
return (dispatch, getState, { getIntl }) => {
const { formatMessage } = getIntl(getState());
dispatch(this.deletePlanPending(planName));
browserHistory.push('/plans/list');
MistralApiService.runAction(MistralConstants.PLAN_DELETE, { container: planName })
.then(response => {
dispatch(this.deletePlanSuccess(planName));
dispatch(NotificationActions.notify({
title: 'Plan Deleted',
message: `The plan ${planName} was successfully deleted.`,
title: formatMessage(messages.planDeletedNotificationTitle),
message: formatMessage(messages.planDeletedNotificationMessage, { planName: planName }),
type: 'success'
}));
dispatch(CurrentPlanActions.detectPlan());
@ -374,11 +411,12 @@ export default {
},
deployPlanFinished(payload) {
return (dispatch) => {
return (dispatch, getState, { getIntl }) => {
const { formatMessage } = getIntl(getState());
if(payload.status === 'FAILED') {
dispatch(this.deployPlanFailed(payload.execution.input.container));
dispatch(NotificationActions.notify({
title: 'Deployment failed',
title: formatMessage(messages.deploymentFailedNotificationTitle),
message: payload.message,
type: 'error'
}));

View File

@ -1,4 +1,5 @@
import { browserHistory } from 'react-router';
import { defineMessages } from 'react-intl';
import { normalize, arrayOf } from 'normalizr';
import { Map } from 'immutable';
@ -12,6 +13,17 @@ import ValidationsActions from './ValidationsActions';
import MistralConstants from '../constants/MistralConstants';
import logger from '../services/logger';
const messages = defineMessages({
registrationNotificationTitle: {
id: 'RegisterNodesActions.registrationNotificationTitle',
defaultMessage: 'Nodes Registration Complete'
},
registrationNotificationMessage: {
id: 'RegisterNodesActions.registrationNotificationMessage',
defaultMessage: 'The nodes were successfully registered.'
}
});
export default {
addNode(node) {
return {
@ -84,7 +96,8 @@ export default {
},
nodesRegistrationFinished(messagePayload) {
return (dispatch, getState) => {
return (dispatch, getState, { getIntl }) => {
const { formatMessage } = getIntl(getState());
const registeredNodes = normalize(messagePayload.registered_nodes,
arrayOf(nodeSchema)).entities.nodes || Map();
dispatch(NodesActions.addNodes(registeredNodes));
@ -99,8 +112,8 @@ export default {
case 'SUCCESS': {
dispatch(NotificationActions.notify({
type: 'success',
title: 'Nodes Registration Complete',
message: 'The nodes were successfully registered'
title: formatMessage(messages.registrationNotificationTitle),
message: formatMessage(messages.registrationNotificationMessage)
}));
dispatch(this.nodesRegistrationSuccess());
browserHistory.push('/nodes/registered');

View File

@ -1,4 +1,5 @@
import { createSelector } from 'reselect';
import { IntlProvider } from 'react-intl';
const languageSelector = state => state.i18n.get('language');
@ -11,3 +12,11 @@ export const getLanguage = createSelector(
export const getMessages = createSelector(
[languageSelector, messagesSelector], (language, messages) => messages[language]
);
export const getIntl = createSelector(
[languageSelector, messagesSelector], (language, messages) => {
const intlProvider = new IntlProvider({ locale: language, messages: messages[language] }, {});
const { intl } = intlProvider.getChildContext();
return intl;
}
);

View File

@ -4,6 +4,7 @@ import createLogger from 'redux-logger';
import logger from './services/logger';
import appReducer from './reducers/appReducer';
import { getIntl } from './selectors/i18n';
const loggerMiddleware = createLogger({
@ -15,7 +16,7 @@ const store = createStore(
appReducer,
{},
applyMiddleware(
thunkMiddleware,
thunkMiddleware.withExtraArgument({ getIntl }),
loggerMiddleware
)
);