diff --git a/src/index.html b/src/index.html index 6aeba279e..dfb90e6c7 100644 --- a/src/index.html +++ b/src/index.html @@ -6,7 +6,7 @@ console.redhat.com - + diff --git a/src/js/App/Header/Tools.js b/src/js/App/Header/Tools.js index 406a1e5c4..09c5a80aa 100644 --- a/src/js/App/Header/Tools.js +++ b/src/js/App/Header/Tools.js @@ -4,6 +4,7 @@ import { Badge, Button, Divider, DropdownItem, ToolbarItem, ToolbarGroup, Switch import QuestionCircleIcon from '@patternfly/react-icons/dist/js/icons/question-circle-icon'; import CogIcon from '@patternfly/react-icons/dist/js/icons/cog-icon'; import RedhatIcon from '@patternfly/react-icons/dist/js/icons/redhat-icon'; +import BellIcon from '@patternfly/react-icons/dist/js/icons/bell-icon'; import UserToggle from './UserToggle'; import ToolbarToggle from './ToolbarToggle'; import HeaderAlert from './HeaderAlert'; @@ -11,6 +12,7 @@ import cookie from 'js-cookie'; import { getUrl, getSection, isBeta } from '../../utils'; import { spinUpStore } from '../../redux-config'; import classnames from 'classnames'; +import { useDispatch } from 'react-redux'; export const switchRelease = (isBeta, pathname) => { cookie.set('cs_toggledRelease', 'true'); @@ -82,6 +84,8 @@ const Tools = () => { isDemoAcc: false, }); + const dispatch = useDispatch(); + useEffect(() => { window.insights.chrome.auth.getUser().then((user) => { /* Disable settings/cog icon when a user doesn't have an account number */ @@ -178,6 +182,11 @@ const Tools = () => { spaceItems={{ default: 'spaceItemsNone' }} widget-type="InsightsToolbar" > + + + {isBeta() && ( beta diff --git a/src/js/App/Notifications/index.js b/src/js/App/Notifications/index.js new file mode 100644 index 000000000..74314d72c --- /dev/null +++ b/src/js/App/Notifications/index.js @@ -0,0 +1,63 @@ +import React, { useEffect, useState } from 'react'; +import { + Drawer, + DrawerActions, + DrawerCloseButton, + DrawerContent, + DrawerContentBody, + DrawerPanelContent, + NotificationDrawer, + NotificationDrawerBody, + NotificationDrawerGroup, + NotificationDrawerGroupList, + NotificationDrawerHeader, + NotificationDrawerList, + NotificationDrawerListItem, + NotificationDrawerListItemBody, + NotificationDrawerListItemHeader, +} from '@patternfly/react-core'; +import { useDispatch, useSelector } from 'react-redux'; + +const NotificationsContent = () => { + const [isBroadcastExpanded, setBroadcastExpanded] = useState(false); + const notifications = useSelector(({ chrome: { notifications } }) => notifications || []); + const dispatch = useDispatch(); + return ( + + + + + dispatch({ type: 'toggle-notifications-drawer' })} /> + + + + + setBroadcastExpanded(value)} isExpanded={isBroadcastExpanded} title="Boradcast"> + + {notifications.map(({ variant = 'info', title, description }, idx) => ( + + + {description} + + ))} + + + + + + + ); +}; + +const Notifications = ({ children }) => { + const isNotificationsDrawerOpen = useSelector(({ chrome: { isNotificationsDrawerOpen } }) => isNotificationsDrawerOpen); + return ( + + }> + {children} + + + ); +}; + +export default Notifications; diff --git a/src/js/App/RootApp/DefaultLayout.js b/src/js/App/RootApp/DefaultLayout.js index 0b205afbd..48f76ced8 100644 --- a/src/js/App/RootApp/DefaultLayout.js +++ b/src/js/App/RootApp/DefaultLayout.js @@ -18,6 +18,7 @@ import '../Sidenav/Navigation/Navigation.scss'; import './DefaultLayout.scss'; import { CROSS_ACCESS_ACCOUNT_NUMBER } from '../../consts'; import { getUrl } from '../../utils'; +import Notifications from '../Notifications'; const ShieldedRoot = memo( ({ hideNav, insightsContentRef, isGlobalFilterEnabled, initialized, Sidebar }) => { @@ -85,12 +86,14 @@ const ShieldedRoot = memo( } sidebar={hideNav ? undefined : } > -
- {isGlobalFilterEnabled && } - {selectedAccountNumber &&
Viewing as Account {selectedAccountNumber}
} - -
-
+ +
+ {isGlobalFilterEnabled && } + {selectedAccountNumber &&
Viewing as Account {selectedAccountNumber}
} + +
+
+
); }, diff --git a/src/js/App/RootApp/RootApp.js b/src/js/App/RootApp/RootApp.js index d1ea80fad..4fc40c7b0 100644 --- a/src/js/App/RootApp/RootApp.js +++ b/src/js/App/RootApp/RootApp.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; import { Router } from 'react-router-dom'; import { HelpTopicContainer, QuickStartContainer } from '@patternfly/quickstarts'; @@ -12,11 +12,73 @@ import useQuickstartsStates from '../QuickStart/useQuickstartsStates'; import { populateQuickstartsCatalog } from '../../redux/actions'; import { LazyQuickStartCatalog } from '../QuickStart/LazyQuickStartCatalog'; import useHelpTopicState from '../QuickStart/useHelpTopicState'; +import Notifications from '../Notifications'; + +const useEvents = () => { + console.log('Use events was for some reason triggered'); + const events = useRef({}); + const conn = useRef(); + const activeModule = useSelector(({ chrome: { activeModule } }) => activeModule); + const dispatch = useDispatch(); + function handleEvent(payload) { + if (payload.type === 'new-notification') { + dispatch({ type: 'add-notification', payload }); + } else if (payload.type === 'invalidate') { + Object.values(events.current) + .flat() + .forEach((cb) => { + cb(payload); + }); + } + console.log(events); + } + + /** + * Establish connection + */ + useEffect(() => { + const x = new WebSocket('ws://localhost:8080/ws'); + x.onmessage = (messageEvent) => { + const { data } = messageEvent; + try { + const payload = JSON.parse(data); + handleEvent(payload); + } catch (error) { + console.error('Unable to parse WS message data: ', error); + } + }; + conn.current = x; + }, []); + + const registerEvent = useCallback( + (type, app, entity, cb) => { + if (typeof app !== 'string' || typeof type !== 'string' || typeof entity !== 'string' || typeof cb !== 'function') { + throw new Error('Invalid registerEvents parameters'); + } + const listener = (data) => { + if (data.type === type && data.app === app && data.entity === entity) { + cb(data); + } + }; + if (Array.isArray(events.current[activeModule])) { + events.current[activeModule].push(listener); + } else { + events.current[activeModule] = [listener]; + } + }, + [activeModule] + ); + + return registerEvent; +}; const RootApp = (props) => { + const dispatch = useDispatch(); + + const registerEvent = useEvents(); + const { allQuickStartStates, setAllQuickStartStates, activeQuickStartID, setActiveQuickStartID } = useQuickstartsStates(); const { helpTopics, addHelpTopics, disableTopics, enableTopics } = useHelpTopicState(); - const dispatch = useDispatch(); const quickStarts = useSelector( ({ chrome: { @@ -69,7 +131,13 @@ const RootApp = (props) => { - + diff --git a/src/js/App/RootApp/ScalprumRoot.js b/src/js/App/RootApp/ScalprumRoot.js index 73a8ff488..2782f19ff 100644 --- a/src/js/App/RootApp/ScalprumRoot.js +++ b/src/js/App/RootApp/ScalprumRoot.js @@ -1,4 +1,4 @@ -import React, { lazy, Suspense, useContext, useEffect, useState } from 'react'; +import React, { lazy, Suspense, useCallback, useContext, useEffect, useRef, useState } from 'react'; import { ScalprumProvider } from '@scalprum/react-core'; import PropTypes from 'prop-types'; import { useSelector, useDispatch } from 'react-redux'; @@ -21,7 +21,7 @@ const loaderWrapper = (Component, props = {}) => ( ); -const ScalprumRoot = ({ config, helpTopicsAPI, quickstartsAPI, ...props }) => { +const ScalprumRoot = ({ registerEvent, config, helpTopicsAPI, quickstartsAPI, ...props }) => { const { setActiveHelpTopicByName, helpTopics, activeHelpTopic } = useContext(HelpTopicContext); const [activeTopicName, setActiveTopicName] = useState(); const [prevActiveTopic, setPrevActiveTopic] = useState(activeHelpTopic?.name); @@ -91,6 +91,7 @@ const ScalprumRoot = ({ config, helpTopicsAPI, quickstartsAPI, ...props }) => { setActiveTopic(''); }, }, + registerEvent, chromeHistory: history, }, }} @@ -123,6 +124,7 @@ ScalprumRoot.propTypes = { toggle: PropTypes.func.isRequired, Catalog: PropTypes.elementType.isRequired, }).isRequired, + registerEvent: PropTypes.func.isRequired, }; export default ScalprumRoot; diff --git a/src/js/redux/index.js b/src/js/redux/index.js index 056042efb..da1ea69b7 100644 --- a/src/js/redux/index.js +++ b/src/js/redux/index.js @@ -19,6 +19,8 @@ import { populateQuickstartsReducer, disableQuickstartsReducer, documentTitleReducer, + notificationsDrawerReducer, + addNewNotificationReducer, } from './reducers'; import { onGetAllTags, @@ -80,6 +82,8 @@ const reducers = { [POPULATE_QUICKSTARTS_CATALOG]: populateQuickstartsReducer, [DISABLE_QUICKSTARTS]: disableQuickstartsReducer, [UPDATE_DOCUMENT_TITLE_REDUCER]: documentTitleReducer, + 'toggle-notifications-drawer': notificationsDrawerReducer, + 'add-notification': addNewNotificationReducer, }; const globalFilter = { @@ -109,6 +113,7 @@ export default function () { quickstarts: { quickstarts: {}, }, + notifications: [], }, action ) => applyReducerHash(reducers)(state, action), diff --git a/src/js/redux/reducers.js b/src/js/redux/reducers.js index e0d5c1384..8ef33be70 100644 --- a/src/js/redux/reducers.js +++ b/src/js/redux/reducers.js @@ -201,3 +201,17 @@ export function documentTitleReducer(state, { payload }) { documentTitle: payload, }; } + +export function notificationsDrawerReducer(state) { + return { + ...state, + isNotificationsDrawerOpen: !state.isNotificationsDrawerOpen, + }; +} + +export function addNewNotificationReducer(state, { payload }) { + return { + ...state, + notifications: [...(state.notifications || []), payload], + }; +}