Skip to content

Commit d706b51

Browse files
authored
feat: add system-wide status alert to all headers (#701)
Adds a sitewide warning banner to Header, StudioHeader, and LearningHeader. Controlled by three settings: - STATUS_ALERT_ENABLED: runtime on/off from MFE config endpoint - STATUS_ALERT_MESSAGE: runtime message text from MFE config endpoint https://2u-internal.atlassian.net/browse/BOMS-562
1 parent df7bf6a commit d706b51

10 files changed

Lines changed: 184 additions & 0 deletions

File tree

docs/using_custom_header.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,23 @@ Some Important Notes
109109
- Intl formatted strings should be passed in content attribute.
110110
- Only menu items in the main menu can be disabled.
111111
- Menu items in the main menu and user menu can have ``isActive`` prop.
112+
113+
Status Alert
114+
------------
115+
116+
A site-wide status banner can be displayed above the header to surface maintenance notices or other operational messages.
117+
118+
- The banner cannot be dismissed.
119+
- The same behavior is available in the ``Header``, ``StudioHeader``, and ``LearningHeader`` components.
120+
121+
The following settings are supplied at runtime via the MFE config api, which allows it to be set globally (using LMS setting ``MFE_CONFIG``) or as a per-MFE override (using LMS setting ``MFE_CONFIG_OVERRIDES``).
122+
123+
``STATUS_ALERT_ENABLED``
124+
************************
125+
126+
Runtime setting supplied via the MFE config api. Set to ``true`` to activate the banner. Defaults to off when absent.
127+
128+
``STATUS_ALERT_MESSAGE``
129+
************************
130+
131+
Runtime setting supplied via the MFE config api. The text shown in the banner. The banner is suppressed when this is empty or absent.

src/Header.jsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { useEnterpriseConfig } from '@2uinc/frontend-enterprise-utils';
1616
import PropTypes from 'prop-types';
1717
import DesktopHeader from './DesktopHeader';
1818
import MobileHeader from './MobileHeader';
19+
import { StatusAlert } from './status-alert';
1920

2021
import messages from './Header.messages';
2122

@@ -199,8 +200,13 @@ const Header = ({
199200
};
200201
}
201202

203+
const showStatusAlert = getConfig().STATUS_ALERT_ENABLED
204+
&& getConfig().STATUS_ALERT_MESSAGE;
205+
const statusAlertMessage = getConfig().STATUS_ALERT_MESSAGE;
206+
202207
return (
203208
<>
209+
{showStatusAlert && <StatusAlert message={statusAlertMessage} />}
204210
<Responsive maxWidth={769}>
205211
<MobileHeader {...props} />
206212
</Responsive>

src/Header.test.jsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,4 +229,42 @@ describe('<Header />', () => {
229229
expect(wrapper.toJSON()).toMatchSnapshot();
230230
});
231231
});
232+
233+
describe('status alert', () => {
234+
afterEach(() => {
235+
getConfig.mockReturnValue({});
236+
});
237+
238+
const STATUS_ALERT_MESSAGE = 'System maintenance in progress';
239+
const contextValue = {
240+
authenticatedUser: null,
241+
config: APP_CONTEXT_CONFIG,
242+
};
243+
244+
it('shows the banner when runtime settings are enabled', () => {
245+
getConfig.mockReturnValue({
246+
STATUS_ALERT_ENABLED: true,
247+
STATUS_ALERT_MESSAGE,
248+
});
249+
render(<HeaderContext width={{ width: 1280 }} contextValue={contextValue} />);
250+
expect(screen.getByText(STATUS_ALERT_MESSAGE)).toBeVisible();
251+
});
252+
253+
it('hides the banner when runtime config disables it', () => {
254+
getConfig.mockReturnValue({
255+
STATUS_ALERT_ENABLED: false,
256+
STATUS_ALERT_MESSAGE,
257+
});
258+
render(<HeaderContext width={{ width: 1280 }} contextValue={contextValue} />);
259+
expect(screen.queryByText(STATUS_ALERT_MESSAGE)).not.toBeInTheDocument();
260+
});
261+
262+
it('hides the banner when the message is absent', () => {
263+
getConfig.mockReturnValue({
264+
STATUS_ALERT_ENABLED: true,
265+
});
266+
render(<HeaderContext width={{ width: 1280 }} contextValue={contextValue} />);
267+
expect(screen.queryByText(STATUS_ALERT_MESSAGE)).not.toBeInTheDocument();
268+
});
269+
});
232270
});

src/learning-header/LearningHeader.jsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import messages from './messages';
1414
import lightning from '../lightning';
1515
import AuthenticatedUser from './AuthenticatedUser';
1616
import { fetchUnifiedTranslationToggleEnabled } from './site-language/data';
17+
import { StatusAlert } from '../status-alert';
1718

1819
ensureConfig([
1920
'ACCOUNT_SETTINGS_URL',
@@ -83,8 +84,13 @@ const LearningHeader = ({
8384
);
8485
}
8586

87+
const showStatusAlert = getConfig().STATUS_ALERT_ENABLED
88+
&& getConfig().STATUS_ALERT_MESSAGE;
89+
const statusAlertMessage = getConfig().STATUS_ALERT_MESSAGE;
90+
8691
return (
8792
<header className="learning-header">
93+
{showStatusAlert && <StatusAlert message={statusAlertMessage} />}
8894
<a className="sr-only sr-only-focusable" href="#main-content">{intl.formatMessage(messages.skipNavLink)}</a>
8995
<div className="px-4 py-2.5 d-flex align-items-center learning-header-container">
9096
{headerLogo}

src/learning-header/LearningHeader.test.jsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
22
import { AppContext } from '@edx/frontend-platform/react';
3+
import { mergeConfig } from '@edx/frontend-platform';
34
import {
45
fireEvent, initializeMockApp, render, screen, waitFor,
56
} from '../setupTest';
@@ -103,4 +104,42 @@ describe('Header', () => {
103104
expect(screen.queryByTestId('site-language-button')).not.toBeInTheDocument();
104105
});
105106
});
107+
108+
describe('status alert', () => {
109+
const STATUS_ALERT_MESSAGE = 'System maintenance in progress';
110+
111+
afterEach(() => {
112+
mergeConfig({
113+
STATUS_ALERT_ENABLED: false,
114+
STATUS_ALERT_MESSAGE: null,
115+
});
116+
});
117+
118+
it('shows the banner when runtime settings are enabled', () => {
119+
mergeConfig({
120+
STATUS_ALERT_ENABLED: true,
121+
STATUS_ALERT_MESSAGE,
122+
});
123+
render(<Header />);
124+
expect(screen.getByText(STATUS_ALERT_MESSAGE)).toBeVisible();
125+
});
126+
127+
it('hides the banner when runtime config disables it', () => {
128+
mergeConfig({
129+
STATUS_ALERT_ENABLED: false,
130+
STATUS_ALERT_MESSAGE,
131+
});
132+
render(<Header />);
133+
expect(screen.queryByText(STATUS_ALERT_MESSAGE)).not.toBeInTheDocument();
134+
});
135+
136+
it('hides the banner when the message is absent', () => {
137+
mergeConfig({
138+
STATUS_ALERT_ENABLED: true,
139+
STATUS_ALERT_MESSAGE: null,
140+
});
141+
render(<Header />);
142+
expect(screen.queryByText(STATUS_ALERT_MESSAGE)).not.toBeInTheDocument();
143+
});
144+
});
106145
});

src/status-alert/StatusAlert.jsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { Icon, PageBanner } from '@openedx/paragon';
4+
import { WarningFilled } from '@openedx/paragon/icons';
5+
6+
const StatusAlert = ({ message }) => (
7+
<PageBanner variant="warning">
8+
<Icon src={WarningFilled} className="mie-2" />
9+
{message}
10+
</PageBanner>
11+
);
12+
13+
StatusAlert.propTypes = {
14+
message: PropTypes.string.isRequired,
15+
};
16+
17+
export default StatusAlert;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
4+
import StatusAlert from './StatusAlert';
5+
6+
describe('StatusAlert', () => {
7+
it('renders the message text', () => {
8+
render(<StatusAlert message="System maintenance in progress" />);
9+
expect(screen.getByText('System maintenance in progress')).toBeVisible();
10+
});
11+
12+
it('renders the warning icon', () => {
13+
const { container } = render(<StatusAlert message="System maintenance in progress" />);
14+
expect(container.querySelector('svg')).toBeInTheDocument();
15+
});
16+
});

src/status-alert/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as StatusAlert } from './StatusAlert';

src/studio-header/StudioHeader.jsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ensureConfig } from '@edx/frontend-platform';
66

77
import MobileHeader from './MobileHeader';
88
import HeaderBody from './HeaderBody';
9+
import { StatusAlert } from '../status-alert';
910

1011
ensureConfig([
1112
'STUDIO_BASE_URL',
@@ -36,8 +37,13 @@ const StudioHeader = ({
3637
outlineLink,
3738
};
3839

40+
const showStatusAlert = config.STATUS_ALERT_ENABLED
41+
&& config.STATUS_ALERT_MESSAGE;
42+
const statusAlertMessage = config.STATUS_ALERT_MESSAGE;
43+
3944
return (
4045
<div className="studio-header">
46+
{showStatusAlert && <StatusAlert message={statusAlertMessage} />}
4147
<a className="nav-skip sr-only sr-only-focusable" href="#main">Skip to content</a>
4248
<Responsive maxWidth={841}>
4349
<MobileHeader {...props} />

src/studio-header/StudioHeader.test.jsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const authenticatedUser = {
2424
};
2525
let currentUser;
2626
let screenWidth = 1280;
27+
let extraConfig = {};
2728

2829
const RootWrapper = ({
2930
...props
@@ -36,6 +37,7 @@ const RootWrapper = ({
3637
SITE_NAME: process.env.SITE_NAME,
3738
STUDIO_BASE_URL: process.env.STUDIO_BASE_URL,
3839
LOGIN_URL: process.env.LOGIN_URL,
40+
...extraConfig,
3941
},
4042
}), []);
4143
const responsiveContextValue = useMemo(() => ({ width: screenWidth }), []);
@@ -77,6 +79,7 @@ describe('Header', () => {
7779
beforeEach(() => {
7880
jest.clearAllMocks();
7981
currentUser = authenticatedUser;
82+
extraConfig = {};
8083
});
8184
describe('desktop', () => {
8285
it('course lock up should be visible', () => {
@@ -210,4 +213,36 @@ describe('Header', () => {
210213
expect(desktopMenu).toBeNull();
211214
});
212215
});
216+
217+
describe('status alert', () => {
218+
const STATUS_ALERT_MESSAGE = 'System maintenance in progress';
219+
220+
beforeEach(() => { screenWidth = 1280; });
221+
222+
it('shows the banner when runtime settings are enabled', () => {
223+
extraConfig = {
224+
STATUS_ALERT_ENABLED: true,
225+
STATUS_ALERT_MESSAGE,
226+
};
227+
const { getByText } = render(<RootWrapper {...props} />);
228+
expect(getByText(STATUS_ALERT_MESSAGE)).toBeVisible();
229+
});
230+
231+
it('hides the banner when runtime config disables it', () => {
232+
extraConfig = {
233+
STATUS_ALERT_ENABLED: false,
234+
STATUS_ALERT_MESSAGE,
235+
};
236+
const { queryByText } = render(<RootWrapper {...props} />);
237+
expect(queryByText(STATUS_ALERT_MESSAGE)).toBeNull();
238+
});
239+
240+
it('hides the banner when the message is absent', () => {
241+
extraConfig = {
242+
STATUS_ALERT_ENABLED: true,
243+
};
244+
const { queryByText } = render(<RootWrapper {...props} />);
245+
expect(queryByText(STATUS_ALERT_MESSAGE)).toBeNull();
246+
});
247+
});
213248
});

0 commit comments

Comments
 (0)