From dd15a82a77822a121e0f82781db501375c0dc752 Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Wed, 5 Apr 2023 20:40:13 -0400 Subject: [PATCH] refactor(bannerMessages): sw-884 generic banners * bannerMessages, replace with generic banners * bannerMessagesContext, hooks for remove, add banners * productView, allow banner messages for all products * redux, actions, types, reducer, restructure --- public/locales/en-US.json | 6 +- src/components/README.md | 102 +++- .../__snapshots__/bannerMessages.test.js.snap | 530 +++++++++--------- .../bannerMessagesContext.test.js.snap | 76 +-- .../__tests__/bannerMessages.test.js | 22 +- .../__tests__/bannerMessagesContext.test.js | 104 ++-- .../bannerMessages/bannerMessages.js | 111 +--- .../bannerMessages/bannerMessagesContext.js | 168 ++++-- .../__tests__/__snapshots__/i18n.test.js.snap | 13 - .../__snapshots__/productView.test.js.snap | 79 +-- src/components/productView/productView.js | 4 +- src/redux/README.md | 34 +- .../actions/__tests__/rhsmActions.test.js | 14 +- src/redux/actions/rhsmActions.js | 22 - .../messagesReducer.test.js.snap | 50 +- .../__tests__/messagesReducer.test.js | 63 +-- src/redux/reducers/messagesReducer.js | 29 +- .../__snapshots__/index.test.js.snap | 16 +- src/redux/types/index.js | 3 + src/redux/types/messageTypes.js | 15 + src/redux/types/rhsmTypes.js | 5 +- tests/__snapshots__/code.test.js.snap | 1 + 22 files changed, 696 insertions(+), 771 deletions(-) create mode 100644 src/redux/types/messageTypes.js diff --git a/public/locales/en-US.json b/public/locales/en-US.json index f200296a2..2fbbf2b77 100644 --- a/public/locales/en-US.json +++ b/public/locales/en-US.json @@ -3,11 +3,7 @@ "pending": "Authenticating...", "maintenanceCopy": "We are currently undergoing scheduled maintenance and will be back shortly. Thank you for your patience." }, - "curiosity-banner": { - "dataMismatchTitle": "Cloud provider mismatch", - "dataMismatchMessage": "There might be inconsistencies between data in the graph and your current systems table. The issue is currently being resolved.", - "dataMismatchMessage_cloudigradeMismatch": "<0>View recommended actions to take in order to improve {{appName}} reporting." - }, + "curiosity-banner": {}, "curiosity-graph": { "cardActionTotal": "{{total}} used", "cardActionTotal_Cores": "{{total}} core hours used", diff --git a/src/components/README.md b/src/components/README.md index b620cb837..e75a4692f 100644 --- a/src/components/README.md +++ b/src/components/README.md @@ -365,9 +365,9 @@ Render banner messages. propsobject - props.messagesArray + props.useBannerMessagesfunction - props.useGetAppMessagesfunction + props.useRemoveBannerMessagesfunction @@ -391,10 +391,18 @@ Default props. ## BannerMessagesContext - -### BannerMessagesContext~useGetAppMessages(options) ⇒ Object -Get app messages. +* [BannerMessagesContext](#BannerMessages.module_BannerMessagesContext) + * [~useBannerMessages(options)](#BannerMessages.module_BannerMessagesContext..useBannerMessages) ⇒ Object + * [~useRemoveBannerMessages(options)](#BannerMessages.module_BannerMessagesContext..useRemoveBannerMessages) ⇒ function + * [~useSetBannerMessages(options)](#BannerMessages.module_BannerMessagesContext..useSetBannerMessages) ⇒ function + * [~removeBannerMessages](#BannerMessages.module_BannerMessagesContext..removeBannerMessages) : function + * [~setBannerMessages](#BannerMessages.module_BannerMessagesContext..setBannerMessages) : function + + + +### BannerMessagesContext~useBannerMessages(options) ⇒ Object +Retrieve, set and remove application banner messages from state. **Kind**: inner method of [BannerMessagesContext](#BannerMessages.module_BannerMessagesContext) @@ -407,15 +415,93 @@ Get app messages. - + + + + +
optionsobject
options.getMessageReportsfunctionoptions.useProductfunction
options.useSelectorfunction
+ + + +### BannerMessagesContext~useRemoveBannerMessages(options) ⇒ function +Provide a callback for removing application banner messages from state. + +**Kind**: inner method of [BannerMessagesContext](#BannerMessages.module_BannerMessagesContext) + + + + + + + + + - + + +
ParamType
optionsobject
options.useDispatchfunction
options.useProductfunction
options.useProductQueryfunctionoptions.useBannerMessagesfunction
+ + + +### BannerMessagesContext~useSetBannerMessages(options) ⇒ function +Provide a callback for setting application banner messages from state. + +**Kind**: inner method of [BannerMessagesContext](#BannerMessages.module_BannerMessagesContext) + + + + + + + + + - + + + + + + +
ParamType
optionsobject
options.useSelectorsResponsefunctionoptions.useDispatchfunction
options.useProductfunction
options.useBannerMessagesfunction
+ + + +### BannerMessagesContext~removeBannerMessages : function +Remove a banner message from state. + +**Kind**: inner typedef of [BannerMessagesContext](#BannerMessages.module_BannerMessagesContext) + + + + + + + + + + +
ParamType
idTitlestring
+ + + +### BannerMessagesContext~setBannerMessages : function +Set application messages for banner display + +**Kind**: inner typedef of [BannerMessagesContext](#BannerMessages.module_BannerMessagesContext) + + + + + + + + +
ParamType
messagesArray.<{id: string, message: string, title: string, variant: string}> | Object
diff --git a/src/components/bannerMessages/__tests__/__snapshots__/bannerMessages.test.js.snap b/src/components/bannerMessages/__tests__/__snapshots__/bannerMessages.test.js.snap index 975a03186..888c643a7 100644 --- a/src/components/bannerMessages/__tests__/__snapshots__/bannerMessages.test.js.snap +++ b/src/components/bannerMessages/__tests__/__snapshots__/bannerMessages.test.js.snap @@ -1,295 +1,323 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`BannerMessages Component should handle closing messages from state: state messages, OFF 1`] = `null`; +exports[`BannerMessages Component should handle closing messages from state: state messages, OFF id 1`] = ` +[ + [ + "loremIpsum", + ], +] +`; exports[`BannerMessages Component should handle closing messages from state: state messages, ON 1`] = `
- - } - key="dolorSit" - title="Dolor sit title" - variant="info" + -
- -
- - - - - -
-
-

- - Info alert: - - Dolor sit title -

-
- -
+ +

+ + Info alert: + + Lorem ipsum title +

+
+ + - - - -
-
- Dolor sit message -
-
-
+ + + + + +
+
+ Lorem ipsum message +
+ + + + + +
`; exports[`BannerMessages Component should render a basic component: basic 1`] = `
- - } - key="loremIpsum" - title="Lorem ipsum title" - variant="info" + -
- -
- - - - - -
-
-

- - Info alert: - - Lorem ipsum title -

-
- -
+ +

+ + Info alert: + + Lorem ipsum title +

+
+ + - - - -
-
- Lorem ipsum message -
-
-
+ + + + + +
+
+ Lorem ipsum message +
+ + + + + +
`; diff --git a/src/components/bannerMessages/__tests__/__snapshots__/bannerMessagesContext.test.js.snap b/src/components/bannerMessages/__tests__/__snapshots__/bannerMessagesContext.test.js.snap index 7d6f3c91c..0622a31d6 100644 --- a/src/components/bannerMessages/__tests__/__snapshots__/bannerMessagesContext.test.js.snap +++ b/src/components/bannerMessages/__tests__/__snapshots__/bannerMessagesContext.test.js.snap @@ -1,51 +1,51 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`BannerMessagesContext should apply a hook for retrieving messages data from a selectors: error response 1`] = ` -{ - "data": { - "cloudigradeMismatch": false, - }, - "error": true, - "fulfilled": undefined, - "pending": undefined, -} +exports[`BannerMessagesContext should apply a hook for retrieving messages data from a selector and apply new messages: dispatch 1`] = ` +[ + [ + { + "bannerMessages": [ + { + "id": "lorem", + "title": "ipsum", + }, + { + "id": "new message", + "title": "new message", + }, + ], + "type": "SET_BANNER_MESSAGES", + "viewId": "dolorSit", + }, + ], +] `; -exports[`BannerMessagesContext should apply a hook for retrieving messages data from a selectors: mock store error response 1`] = ` -{ - "data": { - "cloudigradeMismatch": false, - }, - "error": true, - "fulfilled": false, - "pending": false, -} +exports[`BannerMessagesContext should apply a hook for retrieving messages data from a selector and remove messages: dispatch 1`] = ` +[ + [ + { + "bannerMessages": [], + "type": "SET_BANNER_MESSAGES", + "viewId": "dolorSit", + }, + ], +] `; -exports[`BannerMessagesContext should apply a hook for retrieving messages data from a selectors: mock store success response 1`] = ` -{ - "data": { - "cloudigradeMismatch": true, +exports[`BannerMessagesContext should apply a hook for retrieving messages data from a selector: banner messages 1`] = ` +[ + { + "id": "lorem", + "title": "ipsum", }, - "error": false, - "fulfilled": true, - "pending": false, -} -`; - -exports[`BannerMessagesContext should apply a hook for retrieving messages data from a selectors: success response 1`] = ` -{ - "data": { - "cloudigradeMismatch": false, - }, - "error": undefined, - "fulfilled": true, - "pending": undefined, -} +] `; exports[`BannerMessagesContext should return specific properties: specific properties 1`] = ` { - "useGetAppMessages": [Function], + "useBannerMessages": [Function], + "useRemoveBannerMessages": [Function], + "useSetBannerMessages": [Function], } `; diff --git a/src/components/bannerMessages/__tests__/bannerMessages.test.js b/src/components/bannerMessages/__tests__/bannerMessages.test.js index 844f2ac5e..18b432327 100644 --- a/src/components/bannerMessages/__tests__/bannerMessages.test.js +++ b/src/components/bannerMessages/__tests__/bannerMessages.test.js @@ -5,18 +5,13 @@ import { BannerMessages } from '../bannerMessages'; describe('BannerMessages Component', () => { it('should render a basic component', async () => { const props = { - messages: [ + useBannerMessages: () => [ { id: 'loremIpsum', title: 'Lorem ipsum title', message: 'Lorem ipsum message' } - ], - useGetAppMessages: () => ({ - data: { - loremIpsum: true - } - }) + ] }; const component = await mountHookComponent(); @@ -24,15 +19,16 @@ describe('BannerMessages Component', () => { }); it('should handle closing messages from state', async () => { + const mockRemove = jest.fn(); const props = { - messages: [ + useBannerMessages: () => [ { - id: 'dolorSit', - title: 'Dolor sit title', - message: 'Dolor sit message' + id: 'loremIpsum', + title: 'Lorem ipsum title', + message: 'Lorem ipsum message' } ], - useGetAppMessages: () => ({ data: { dolorSit: true } }) + useRemoveBannerMessages: () => mockRemove }; const component = await mountHookComponent(); @@ -40,6 +36,6 @@ describe('BannerMessages Component', () => { component.find(AlertActionCloseButton).first().simulate('click'); - expect(component.render()).toMatchSnapshot('state messages, OFF'); + expect(mockRemove.mock.calls).toMatchSnapshot('state messages, OFF id'); }); }); diff --git a/src/components/bannerMessages/__tests__/bannerMessagesContext.test.js b/src/components/bannerMessages/__tests__/bannerMessagesContext.test.js index a9045c1cc..884de7a78 100644 --- a/src/components/bannerMessages/__tests__/bannerMessagesContext.test.js +++ b/src/components/bannerMessages/__tests__/bannerMessagesContext.test.js @@ -1,86 +1,60 @@ -import { context, useGetAppMessages } from '../bannerMessagesContext'; -import { rhsmConstants } from '../../../services/rhsm/rhsmConstants'; +import { context, useBannerMessages, useRemoveBannerMessages, useSetBannerMessages } from '../bannerMessagesContext'; describe('BannerMessagesContext', () => { it('should return specific properties', () => { expect(context).toMatchSnapshot('specific properties'); }); - it('should apply a hook for retrieving messages data from a selectors', () => { - const { result: errorResponse } = shallowHook(() => - useGetAppMessages({ - useSelectorsResponse: () => ({ - error: true, - data: { - messages: {} + it('should apply a hook for retrieving messages data from a selector', async () => { + const { result } = await shallowHook(() => + useBannerMessages({ + useSelector: () => [ + { + id: 'lorem', + title: 'ipsum' } - }) + ] }) ); - expect(errorResponse).toMatchSnapshot('error response'); + expect(result).toMatchSnapshot('banner messages'); + }); - const { result: successResponse } = shallowHook(() => - useGetAppMessages({ - useSelectorsResponse: () => ({ - fulfilled: true, - data: { - messages: {} + it('should apply a hook for retrieving messages data from a selector and apply new messages', async () => { + const mockDispatch = jest.fn(); + const { result } = await mountHook(() => + useSetBannerMessages({ + useDispatch: () => mockDispatch, + useProduct: () => ({ productId: 'dolorSit' }), + useBannerMessages: () => [ + { + id: 'lorem', + title: 'ipsum' } - }) + ] }) ); - expect(successResponse).toMatchSnapshot('success response'); - - const { result: mockStoreSuccessResponse } = shallowHook( - () => - useGetAppMessages({ - useProduct: () => ({ productId: 'loremIpsum' }) - }), - { - state: { - messages: { - report: { - loremIpsum: { - fulfilled: true, - data: { - data: [ - { - [rhsmConstants.RHSM_API_RESPONSE_TALLY_CAPACITY_META_TYPES.HAS_CLOUDIGRADE_MISMATCH]: true - } - ] - } - } - } - } - } - } - ); - - expect(mockStoreSuccessResponse).toMatchSnapshot('mock store success response'); + result('new message'); + expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch'); + }); - const { result: mockStoreErrorResponse } = shallowHook( - () => - useGetAppMessages({ - useProduct: () => ({ productId: 'loremIpsum' }) - }), - { - state: { - messages: { - report: { - loremIpsum: { - error: true, - data: { - data: [] - } - } - } + it('should apply a hook for retrieving messages data from a selector and remove messages', async () => { + const mockDispatch = jest.fn(); + const { result } = await mountHook(() => + useRemoveBannerMessages({ + useDispatch: () => mockDispatch, + useProduct: () => ({ productId: 'dolorSit' }), + useBannerMessages: () => [ + { + id: 'lorem', + title: 'ipsum' } - } - } + ] + }) ); - expect(mockStoreErrorResponse).toMatchSnapshot('mock store error response'); + result('ipsum'); + expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch'); }); }); diff --git a/src/components/bannerMessages/bannerMessages.js b/src/components/bannerMessages/bannerMessages.js index f4c82e5f9..3cdb0185b 100644 --- a/src/components/bannerMessages/bannerMessages.js +++ b/src/components/bannerMessages/bannerMessages.js @@ -1,11 +1,7 @@ -import React, { useState } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import { Alert, AlertActionCloseButton, AlertVariant, Button } from '@patternfly/react-core'; -import { ExternalLinkAltIcon } from '@patternfly/react-icons'; -import { useShallowCompareEffect } from 'react-use'; -import { useGetAppMessages } from './bannerMessagesContext'; -import { helpers } from '../../common'; -import { translate } from '../i18n/i18n'; +import { Alert, AlertActionCloseButton, AlertGroup, AlertVariant } from '@patternfly/react-core'; +import { useBannerMessages, useRemoveBannerMessages } from './bannerMessagesContext'; /** * Banner alert messages for a product view. @@ -19,48 +15,33 @@ import { translate } from '../i18n/i18n'; * Render banner messages. * * @param {object} props - * @param {Array} props.messages - * @param {Function} props.useGetAppMessages + * @param {Function} props.useBannerMessages + * @param {Function} props.useRemoveBannerMessages * @returns {React.ReactNode} */ -const BannerMessages = ({ messages, useGetAppMessages: useAliasGetAppMessages }) => { - const [hideAlerts, setHideAlerts] = useState({}); - const [alerts, setAlerts] = useState([]); - const { data: appMessages } = useAliasGetAppMessages(); +const BannerMessages = ({ + useBannerMessages: useAliasBannerMessages, + useRemoveBannerMessages: useAliasRemoveBannerMessages +}) => { + const bannerMessages = useAliasBannerMessages(); + const removeBannerMessages = useAliasRemoveBannerMessages(); - useShallowCompareEffect(() => { - const updatedMessages = []; + if (bannerMessages?.length) { + return ( +
+ + {bannerMessages?.map(({ id, message, title, variant = AlertVariant.info }) => { + const actionClose = removeBannerMessages(id || title)} />; - if (messages.length) { - Object.entries(appMessages).forEach(([key, value]) => { - if (hideAlerts[key] !== true && value === true) { - const message = messages.find(({ id }) => id === key); - - if (message) { - updatedMessages.push({ - key, - ...message - }); - } - } - }); - } - - setAlerts( - updatedMessages.map(({ key, message, title, variant = AlertVariant.info }) => { - const actionClose = setHideAlerts({ ...hideAlerts, [key]: true })} />; - - return ( - - {message} - - ); - }) + return ( + + {message} + + ); + })} + +
); - }, [appMessages, hideAlerts, messages]); - - if (alerts?.length) { - return
{alerts}
; } return null; @@ -69,51 +50,21 @@ const BannerMessages = ({ messages, useGetAppMessages: useAliasGetAppMessages }) /** * Prop types. * - * @type {{useGetAppMessages: Function, messages: Array}} + * @type {{useBannerMessages: Function, useRemoveBannerMessages: Function}} */ BannerMessages.propTypes = { - messages: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string.isRequired, - title: PropTypes.node.isRequired, - message: PropTypes.node.isRequired, - variant: PropTypes.oneOf([...Object.values(AlertVariant)]) - }) - ), - useGetAppMessages: PropTypes.func + useBannerMessages: PropTypes.func, + useRemoveBannerMessages: PropTypes.func }; /** * Default props. * - * @type {{useGetAppMessages: Function, messages: Array}} + * @type {{useBannerMessages: Function, useRemoveBannerMessages: Function}} */ BannerMessages.defaultProps = { - messages: [ - { - id: 'cloudigradeMismatch', - title: translate('curiosity-banner.dataMismatchTitle'), - message: translate( - 'curiosity-banner.dataMismatchMessage', - { - context: helpers.UI_LINK_REPORT_ACCURACY_RECOMMENDATIONS !== '' && 'cloudigradeMismatch', - appName: helpers.UI_DISPLAY_NAME - }, - [ -