feat: allow runtime configuration (#253)

Allows frontend-app-gradebook to be configured at
runtime using the LMS's new MFE Configuration API.

Part of https://github.com/openedx/frontend-wg/issues/103
This commit is contained in:
Diana Olarte
2022-10-17 11:12:42 -05:00
committed by Adolfo R. Brandes
parent 03fa143fc1
commit bccd87fd49
11 changed files with 116 additions and 11 deletions

2
.env
View File

@@ -30,3 +30,5 @@ ENTERPRISE_MARKETING_URL=''
ENTERPRISE_MARKETING_UTM_SOURCE=''
ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
APP_ID=''
MFE_CONFIG_API_URL=''

View File

@@ -37,3 +37,5 @@ ENTERPRISE_MARKETING_URL='http://example.com'
ENTERPRISE_MARKETING_UTM_SOURCE='example.com'
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
APP_ID=''
MFE_CONFIG_API_URL=''

56
package-lock.json generated
View File

@@ -12,7 +12,7 @@
"@edx/brand": "npm:@edx/brand-edx.org@^1.3.2",
"@edx/frontend-component-footer": "^11.1.1",
"@edx/frontend-component-header": "^3.1.1",
"@edx/frontend-platform": "2.3.0",
"@edx/frontend-platform": "2.5.0",
"@edx/paragon": "19.6.0",
"@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-brands-svg-icons": "^5.11.2",
@@ -31,6 +31,7 @@
"query-string": "6.13.0",
"react": "16.14.0",
"react-dom": "16.14.0",
"react-helmet": "^6.1.0",
"react-intl": "^2.9.0",
"react-redux": "^7.1.1",
"react-router": "5.2.0",
@@ -3986,9 +3987,9 @@
}
},
"node_modules/@edx/frontend-platform": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-2.3.0.tgz",
"integrity": "sha512-vZAw3eKJgUvD3wu8QOlCbNvuhe9YOGhdVuiTiFGMJKsYagJNMuQZxTJ2DwPCr7/gprJ65mboisJ3BF5IoFzVJA==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-2.5.0.tgz",
"integrity": "sha512-Ws40TMkxrF9Fz71K8bqp+qui7kXYOBvl8+PYLa1K0lmzwD70FFU73mQBTvgTJKKWcw8VsjK9oJCxmjGvz6Qe1Q==",
"dependencies": {
"@cospired/i18n-iso-languages": "2.2.0",
"@formatjs/intl-pluralrules": "^4.3.3",
@@ -4014,7 +4015,7 @@
"transifex-utils.js": "i18n/scripts/transifex-utils.js"
},
"peerDependencies": {
"@edx/paragon": ">= 10.0.0 < 20.0.0",
"@edx/paragon": ">= 10.0.0 < 21.0.0",
"prop-types": "^15.7.2",
"react": "^16.9.0",
"react-dom": "^16.9.0",
@@ -28541,6 +28542,20 @@
}
}
},
"node_modules/react-helmet": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz",
"integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==",
"dependencies": {
"object-assign": "^4.1.1",
"prop-types": "^15.7.2",
"react-fast-compare": "^3.1.1",
"react-side-effect": "^2.1.0"
},
"peerDependencies": {
"react": ">=16.3.0"
}
},
"node_modules/react-intl": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-2.9.0.tgz",
@@ -28803,6 +28818,14 @@
"isarray": "0.0.1"
}
},
"node_modules/react-side-effect": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz",
"integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==",
"peerDependencies": {
"react": "^16.3.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-style-singleton": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.1.1.tgz",
@@ -37680,9 +37703,9 @@
}
},
"@edx/frontend-platform": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-2.3.0.tgz",
"integrity": "sha512-vZAw3eKJgUvD3wu8QOlCbNvuhe9YOGhdVuiTiFGMJKsYagJNMuQZxTJ2DwPCr7/gprJ65mboisJ3BF5IoFzVJA==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-2.5.0.tgz",
"integrity": "sha512-Ws40TMkxrF9Fz71K8bqp+qui7kXYOBvl8+PYLa1K0lmzwD70FFU73mQBTvgTJKKWcw8VsjK9oJCxmjGvz6Qe1Q==",
"requires": {
"@cospired/i18n-iso-languages": "2.2.0",
"@formatjs/intl-pluralrules": "^4.3.3",
@@ -56733,6 +56756,17 @@
"use-sidecar": "^1.0.5"
}
},
"react-helmet": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz",
"integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==",
"requires": {
"object-assign": "^4.1.1",
"prop-types": "^15.7.2",
"react-fast-compare": "^3.1.1",
"react-side-effect": "^2.1.0"
}
},
"react-intl": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-2.9.0.tgz",
@@ -56936,6 +56970,12 @@
}
}
},
"react-side-effect": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz",
"integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==",
"requires": {}
},
"react-style-singleton": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.1.1.tgz",

View File

@@ -33,7 +33,7 @@
"@edx/brand": "npm:@edx/brand-edx.org@^1.3.2",
"@edx/frontend-component-footer": "^11.1.1",
"@edx/frontend-component-header": "^3.1.1",
"@edx/frontend-platform": "2.3.0",
"@edx/frontend-platform": "2.5.0",
"@edx/paragon": "19.6.0",
"@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-brands-svg-icons": "^5.11.2",
@@ -52,6 +52,7 @@
"query-string": "6.13.0",
"react": "16.14.0",
"react-dom": "16.14.0",
"react-helmet": "^6.1.0",
"react-intl": "^2.9.0",
"react-redux": "^7.1.1",
"react-router": "5.2.0",

View File

@@ -10,9 +10,11 @@ import { routePath } from 'data/constants/app';
import store from 'data/store';
import GradebookPage from 'containers/GradebookPage';
import './App.scss';
import Head from './head/Head';
const App = () => (
<AppProvider store={store}>
<Head />
<Router>
<div>
<Header />

View File

@@ -12,6 +12,7 @@ import store from 'data/store';
import GradebookPage from 'containers/GradebookPage';
import App from './App';
import Head from './head/Head';
jest.mock('react-router-dom', () => ({
BrowserRouter: () => 'BrowserRouter',
@@ -41,7 +42,7 @@ describe('App router component', () => {
beforeEach(() => {
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
el = shallow(<App />);
router = el.childAt(0);
router = el.childAt(1);
});
describe('AppProvider', () => {
test('AppProvider is the parent component, passed the redux store props', () => {
@@ -49,8 +50,13 @@ describe('App router component', () => {
expect(el.props().store).toEqual(store);
});
});
describe('Router', () => {
describe('Head', () => {
test('first child of AppProvider', () => {
expect(el.childAt(0).type()).toBe(Head);
});
});
describe('Router', () => {
test('second child of AppProvider', () => {
expect(router.type()).toBe(Router);
});
test('Header is above/outside-of the routing', () => {

View File

@@ -4,6 +4,7 @@ exports[`App router component snapshot 1`] = `
<AppProvider
store="testStore"
>
<injectIntl(ShimmedIntlComponent) />
<BrowserRouter>
<div>
<Header />

21
src/head/Head.jsx Normal file
View File

@@ -0,0 +1,21 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import messages from './messages';
const Head = ({ intl }) => (
<Helmet>
<title>
{intl.formatMessage(messages['gradebook.page.title'], { siteName: getConfig().SITE_NAME })}
</title>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
);
Head.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(Head);

17
src/head/Head.test.jsx Normal file
View File

@@ -0,0 +1,17 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { Helmet } from 'react-helmet';
import { mount } from 'enzyme';
import { getConfig } from '@edx/frontend-platform';
import Head from './Head';
describe('Head', () => {
const props = {};
it('should match render title tag and favicon with the site configuration values', () => {
mount(<IntlProvider locale="en"><Head {...props} /></IntlProvider>);
const helmet = Helmet.peek();
expect(helmet.title).toEqual(`Gradebook | ${getConfig().SITE_NAME}`);
expect(helmet.linkTags[0].rel).toEqual('shortcut icon');
expect(helmet.linkTags[0].href).toEqual(getConfig().FAVICON_URL);
});
});

11
src/head/messages.js Normal file
View File

@@ -0,0 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'gradebook.page.title': {
id: 'gradebook.page.title',
defaultMessage: 'Gradebook | {siteName}',
description: 'Title tag',
},
});
export default messages;

View File

@@ -8,6 +8,8 @@ Enzyme.configure({ adapter: new Adapter() });
// These configuration values are usually set in webpack's EnvironmentPlugin however
// Jest does not use webpack so we need to set these so for testing
process.env.LMS_BASE_URL = 'http://localhost:18000';
process.env.SITE_NAME = 'localhost';
process.env.FAVICON_URL = 'http://localhost:18000/favicon.ico';
jest.mock('@edx/frontend-platform/i18n', () => {
const i18n = jest.requireActual('@edx/frontend-platform/i18n');