From 4f43e65f039a8c8464b35326dcbd11c8a67f777b Mon Sep 17 00:00:00 2001 From: Kyle McCormick Date: Mon, 28 Nov 2022 11:14:32 -0500 Subject: [PATCH] fix: use getConfig in order to support runtime configuration (#286) Before, gradebook was reading config from `process.env` directly, which locked the app into using only static (build-time) configuration. In order to enable dynamic (runtime) configuration, we update gradebook to use frontend-platform's standard configuration interface: `mergeConfig()` and `getConfig()`. Bumps version from 1.5.0 to 1.6.0. (I would normally just do a patch release for a fix, but the version was hasn't been bumped for a while, so adding in full runtime configuration support seemed like it warranted a proper minor version bump.) Co-authored-by: Ghassan Maslamani --- package.json | 2 +- src/components/GradebookHeader/index.jsx | 4 ++-- src/config/index.js | 16 ---------------- src/data/services/lms/urls.js | 4 ++-- src/data/store.js | 4 ++-- src/data/store.test.js | 14 +++++++------- src/index.jsx | 17 +++++++++++++++++ src/index.test.jsx | 17 ++++++++++++++++- src/segment.js | 4 ++-- 9 files changed, 49 insertions(+), 33 deletions(-) delete mode 100644 src/config/index.js diff --git a/package.json b/package.json index 31e4cbf..e59c83a 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@edx/frontend-app-gradebook", - "version": "1.5.0", + "version": "1.6.0", "description": "edx editable gradebook-ui to manipulate grade overrides on subsections", "repository": { "type": "git", diff --git a/src/components/GradebookHeader/index.jsx b/src/components/GradebookHeader/index.jsx index 466620a..1b004cd 100644 --- a/src/components/GradebookHeader/index.jsx +++ b/src/components/GradebookHeader/index.jsx @@ -2,10 +2,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; +import { getConfig } from '@edx/frontend-platform'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { Button } from '@edx/paragon'; -import { configuration } from 'config'; import { views } from 'data/constants/app'; import actions from 'data/actions'; import selectors from 'data/selectors'; @@ -25,7 +25,7 @@ export class GradebookHeader extends React.Component { } lmsInstructorDashboardUrl = courseId => ( - `${configuration.LMS_BASE_URL}/courses/${courseId}/instructor` + `${getConfig().LMS_BASE_URL}/courses/${courseId}/instructor` ); handleToggleViewClick() { diff --git a/src/config/index.js b/src/config/index.js deleted file mode 100644 index 1ed328b..0000000 --- a/src/config/index.js +++ /dev/null @@ -1,16 +0,0 @@ -const configuration = { - BASE_URL: process.env.BASE_URL, - LMS_BASE_URL: process.env.LMS_BASE_URL, - LOGIN_URL: process.env.LOGIN_URL, - LOGOUT_URL: process.env.LOGOUT_URL, - CSRF_TOKEN_API_PATH: process.env.CSRF_TOKEN_API_PATH, - REFRESH_ACCESS_TOKEN_ENDPOINT: process.env.REFRESH_ACCESS_TOKEN_ENDPOINT, - DATA_API_BASE_URL: process.env.DATA_API_BASE_URL, - SECURE_COOKIES: process.env.NODE_ENV !== 'development', - SEGMENT_KEY: process.env.SEGMENT_KEY, - ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME, -}; - -const features = {}; - -export { configuration, features }; diff --git a/src/data/services/lms/urls.js b/src/data/services/lms/urls.js index df09cc5..843dc99 100644 --- a/src/data/services/lms/urls.js +++ b/src/data/services/lms/urls.js @@ -1,9 +1,9 @@ +import { getConfig } from '@edx/frontend-platform'; import { StrictDict } from 'utils'; -import { configuration } from 'config'; import { historyRecordLimit } from './constants'; import { filterQuery, stringifyUrl } from './utils'; -const baseUrl = `${configuration.LMS_BASE_URL}`; +const baseUrl = `${getConfig().LMS_BASE_URL}`; const courseId = window.location.pathname.split('/').filter(Boolean).pop() || ''; diff --git a/src/data/store.js b/src/data/store.js index 09474a9..dfb73b4 100755 --- a/src/data/store.js +++ b/src/data/store.js @@ -4,19 +4,19 @@ import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProductio import { createLogger } from 'redux-logger'; import { createMiddleware } from 'redux-beacon'; import Segment from '@redux-beacon/segment'; +import { getConfig } from '@edx/frontend-platform'; import actions from './actions'; import selectors from './selectors'; import reducers from './reducers'; import eventsMap from './services/segment/mapping'; -import { configuration } from '../config'; export const createStore = () => { const loggerMiddleware = createLogger(); const middleware = [thunkMiddleware, loggerMiddleware]; // Conditionally add the segmentMiddleware only if the SEGMENT_KEY environment variable exists. - if (configuration.SEGMENT_KEY) { + if (getConfig().SEGMENT_KEY) { middleware.push(createMiddleware(eventsMap, Segment())); } const store = redux.createStore( diff --git a/src/data/store.test.js b/src/data/store.test.js index f0d3f7b..6f7cb28 100644 --- a/src/data/store.test.js +++ b/src/data/store.test.js @@ -4,12 +4,12 @@ import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProductio import { createLogger } from 'redux-logger'; import { createMiddleware } from 'redux-beacon'; import Segment from '@redux-beacon/segment'; +import { getConfig } from '@edx/frontend-platform'; import actions from './actions'; import selectors from './selectors'; import reducers from './reducers'; import eventsMap from './services/segment/mapping'; -import { configuration } from '../config'; import exportedStore, { createStore } from './store'; @@ -22,10 +22,10 @@ jest.mock('redux-logger', () => ({ createLogger: () => 'logger', })); jest.mock('redux-thunk', () => 'thunkMiddleware'); -jest.mock('../config', () => ({ - configuration: { +jest.mock('@edx/frontend-platform', () => ({ + getConfig: jest.fn(() => ({ SEGMENT_KEY: 'a-fake-segment-key', - }, + })), })); jest.mock('redux-beacon', () => ({ createMiddleware: jest.fn((map, model) => ({ map, model })), @@ -60,9 +60,9 @@ describe('store aggregator module', () => { }); }); describe('if no SEGMENT_KEY', () => { - const key = configuration.SEGMENT_KEY; + const key = getConfig().SEGMENT_KEY; beforeEach(() => { - configuration.SEGMENT_KEY = false; + getConfig.mockImplementation(() => ({ SEGMENT_KEY: false })); }); it('exports thunk and logger middleware, composed and applied with dev tools', () => { expect(createStore().middleware).toEqual( @@ -70,7 +70,7 @@ describe('store aggregator module', () => { ); }); afterEach(() => { - configuration.SEGMENT_KEY = key; + getConfig.mockImplementation(() => ({ SEGMENT_KEY: key })); }); }); }); diff --git a/src/index.jsx b/src/index.jsx index bb9d1c0..925c196 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -7,6 +7,7 @@ import ReactDOM from 'react-dom'; import { APP_READY, initialize, + mergeConfig, subscribe, } from '@edx/frontend-platform'; import { messages as headerMessages } from '@edx/frontend-component-header'; @@ -20,6 +21,22 @@ subscribe(APP_READY, () => { }); initialize({ + handlers: { + config: () => { + mergeConfig({ + BASE_URL: process.env.BASE_URL, + LMS_BASE_URL: process.env.LMS_BASE_URL, + LOGIN_URL: process.env.LOGIN_URL, + LOGOUT_URL: process.env.LOGOUT_URL, + CSRF_TOKEN_API_PATH: process.env.CSRF_TOKEN_API_PATH, + REFRESH_ACCESS_TOKEN_ENDPOINT: process.env.REFRESH_ACCESS_TOKEN_ENDPOINT, + DATA_API_BASE_URL: process.env.DATA_API_BASE_URL, + SECURE_COOKIES: process.env.NODE_ENV !== 'development', + SEGMENT_KEY: process.env.SEGMENT_KEY, + ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME, + }); + }, + }, messages: [ appMessages, headerMessages, diff --git a/src/index.test.jsx b/src/index.test.jsx index 645c8e2..a50b586 100644 --- a/src/index.test.jsx +++ b/src/index.test.jsx @@ -4,6 +4,7 @@ import ReactDOM from 'react-dom'; import { APP_READY, initialize, + mergeConfig, subscribe, } from '@edx/frontend-platform'; import { messages as headerMessages } from '@edx/frontend-component-header'; @@ -19,6 +20,7 @@ jest.mock('react-dom', () => ({ jest.mock('@edx/frontend-platform', () => ({ APP_READY: 'app-is-ready-key', initialize: jest.fn(), + mergeConfig: jest.fn(), subscribe: jest.fn(), })); jest.mock('@edx/frontend-component-header', () => ({ @@ -46,10 +48,23 @@ describe('app registry', () => { ReactDOM.render(, document.getElementById('root')), ); }); - test('initialize is called with footerMessages and requireAuthenticatedUser', () => { + test('initialize is called with requireAuthenticatedUser, messages, and a config handler', () => { expect(initialize).toHaveBeenCalledWith({ messages: [appMessages, headerMessages, footerMessages], requireAuthenticatedUser: true, + handlers: { + config: expect.any(Function), + }, }); }); + test('initialize config loads LMS_BASE_URL from env', () => { + const oldEnv = process.env; + const initializeArg = initialize.mock.calls[0][0]; + process.env = { ...oldEnv, LMS_BASE_URL: 'http://example.com/fake' }; + initializeArg.handlers.config(); + expect(mergeConfig).toHaveBeenCalledWith( + expect.objectContaining({ LMS_BASE_URL: 'http://example.com/fake' }), + ); + process.env = oldEnv; + }); }); diff --git a/src/segment.js b/src/segment.js index c2eb562..309b20b 100644 --- a/src/segment.js +++ b/src/segment.js @@ -1,6 +1,6 @@ // The code in this file is from Segment's website: // https://segment.com/docs/sources/website/analytics.js/quickstart/ -import { configuration } from './config'; +import { getConfig } from '@edx/frontend-platform'; (function () { // Create a queue, but don't obliterate an existing one! @@ -81,5 +81,5 @@ import { configuration } from './config'; // Load Analytics.js with your key, which will automatically // load the tools you've enabled for your account. Boosh! - analytics.load(configuration.SEGMENT_KEY); + analytics.load(getConfig().SEGMENT_KEY); }());