From 92b96f4009f9738b860c6db4631a03b8c7fa67a9 Mon Sep 17 00:00:00 2001 From: Blake Holifield Date: Tue, 19 Nov 2024 16:02:12 -0500 Subject: [PATCH 1/2] working drawer with websocket; needs toggle and ref fix --- README.md | 8 +- config/webpack.config.js | 3 + docs/wsSubscription.md | 2 +- .../DrawerPanelContent.tsx | 359 ++---------------- src/components/RootApp/ScalprumRoot.tsx | 23 +- src/hooks/useChromeServiceEvents.ts | 2 +- src/layouts/DefaultLayout.tsx | 30 +- 7 files changed, 53 insertions(+), 374 deletions(-) diff --git a/README.md b/README.md index 9220331f0..0a76d6a4b 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ For more detailed information about chrome and what it provides, [look through t ## JavaScript API -Insights Chrome comes with a Javacript API that allows applications to control navigation, global filters, etc. +Insights Chrome comes with a Javascript API that allows applications to control navigation, global filters, etc. Check out the [useChrome hook docs](http://front-end-docs-insights.apps.ocp4.prod.psi.redhat.com/chrome/chrome-api#Chrome) @@ -93,7 +93,7 @@ See [local search development documentation](./docs/localSearchDevelopment.md). ## LocalStorage Debugging -There are some localStorage values for you to enable debuging information or enable some values that are in experimental state. If you want to enable them call `const iqe = insights.chrome.enable.iqe()` for instance to enable such service. This function will return callback to disable such feature so calling `iqe()` will remove such item from localStorage. +There are some localStorage values for you to enable debugging information or enable some values that are in experimental state. If you want to enable them call `const iqe = insights.chrome.enable.iqe()` for instance to enable such service. This function will return callback to disable such feature so calling `iqe()` will remove such item from localStorage. Available function: @@ -102,9 +102,9 @@ Available function: - `jwtDebug` - to enable debugging of JWT - `remediationsDebug` - to enable debug buttons in remediations app - `shortSession` - to enable short session in order to test automatic logouts -- `forcePendo` - to force Pendo initializtion +- `forcePendo` - to force Pendo initialization - `appFilter` - to enable new application filter in any environment -## Futher reading +## Further reading More detailed documentation can be found in the [docs section](https://github.com/redhatinsights/insights-chrome/tree/master/docs) diff --git a/config/webpack.config.js b/config/webpack.config.js index edbb1f78c..326516a90 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -179,6 +179,9 @@ const commonConfig = ({ dev }) => { host: `http://localhost:${process.env.NAV_CONFIG}`, }, }), + '/apps/notifications': { + host: 'http://0.0.0.0:8003', + }, }, }), }, diff --git a/docs/wsSubscription.md b/docs/wsSubscription.md index 4863b88f1..68aa84292 100644 --- a/docs/wsSubscription.md +++ b/docs/wsSubscription.md @@ -46,4 +46,4 @@ const ConsumerComponent = () => { } -``` \ No newline at end of file +``` diff --git a/src/components/NotificationsDrawer/DrawerPanelContent.tsx b/src/components/NotificationsDrawer/DrawerPanelContent.tsx index cbdb88471..3d2ad449b 100644 --- a/src/components/NotificationsDrawer/DrawerPanelContent.tsx +++ b/src/components/NotificationsDrawer/DrawerPanelContent.tsx @@ -1,359 +1,44 @@ -import React, { useContext, useEffect, useMemo, useState } from 'react'; -import { useAtom, useAtomValue, useSetAtom } from 'jotai'; -import { PopoverPosition } from '@patternfly/react-core/dist/dynamic/components/Popover'; -import { Badge } from '@patternfly/react-core/dist/dynamic/components/Badge'; -import { Flex, FlexItem } from '@patternfly/react-core/dist/dynamic/layouts/Flex'; -import { Dropdown, DropdownGroup, DropdownItem, DropdownList } from '@patternfly/react-core/dist/dynamic/components/Dropdown'; -import { MenuToggle, MenuToggleElement } from '@patternfly/react-core/dist/dynamic/components/MenuToggle'; -import { Divider } from '@patternfly/react-core/dist/dynamic/components/Divider'; -import { EmptyState, EmptyStateBody, EmptyStateIcon } from '@patternfly/react-core/dist/dynamic/components/EmptyState'; -import { - NotificationDrawer, - NotificationDrawerBody, - NotificationDrawerHeader, - NotificationDrawerList, -} from '@patternfly/react-core/dist/dynamic/components/NotificationDrawer'; -import { Text } from '@patternfly/react-core/dist/dynamic/components/Text'; -import { Title } from '@patternfly/react-core/dist/dynamic/components/Title'; -import FilterIcon from '@patternfly/react-icons/dist/dynamic/icons/filter-icon'; -import BellSlashIcon from '@patternfly/react-icons/dist/dynamic/icons/bell-slash-icon'; -import EllipsisVIcon from '@patternfly/react-icons/dist/dynamic/icons/ellipsis-v-icon'; -import orderBy from 'lodash/orderBy'; -import { Link, useNavigate } from 'react-router-dom'; -import NotificationItem from './NotificationItem'; +import React, { Fragment, useContext } from 'react'; + +import { NotificationDrawer } from '@patternfly/react-core/dist/dynamic/components/NotificationDrawer'; + import ChromeAuthContext from '../../auth/ChromeAuthContext'; import InternalChromeContext from '../../utils/internalChromeContext'; -import { - NotificationData, - notificationDrawerDataAtom, - notificationDrawerExpandedAtom, - notificationDrawerFilterAtom, - notificationDrawerSelectedAtom, - updateNotificationReadAtom, - updateNotificationSelectedAtom, - updateNotificationsSelectedAtom, -} from '../../state/atoms/notificationDrawerAtom'; -import BulkSelect from '@redhat-cloud-services/frontend-components/BulkSelect'; -import axios from 'axios'; -import { Stack, StackItem } from '@patternfly/react-core/dist/dynamic/layouts/Stack'; - -interface Bundle { - id: string; - name: string; - displayName: string; - children: Bundle[]; -} -interface FilterConfigItem { - title: string; - value: string; -} +import { ScalprumComponent } from '@scalprum/react-core'; export type DrawerPanelProps = { - innerRef: React.Ref; + panelRef: React.Ref; + // toggle: () => void; }; -const EmptyNotifications = ({ isOrgAdmin, onLinkClick }: { onLinkClick: () => void; isOrgAdmin?: boolean }) => ( - - - - No notifications found - - - {isOrgAdmin ? ( - - - There are currently no notifications for you. - - - - Try  - - checking your notification preferences - -  and managing the  - - notification configuration - -  for your organization. - - - - ) : ( - <> - - - There are currently no notifications for you. - - - - Check your Notification Preferences - - - - - View the Event log to see all fired events - - - - Contact your organization administrator - - - - )} - - -); +const DrawerPanelBase = ({ panelRef }: DrawerPanelProps) => { + // toggle drawer will be an api or prop -const DrawerPanelBase = ({ innerRef }: DrawerPanelProps) => { - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false); - const [activeFilters, setActiveFilters] = useAtom(notificationDrawerFilterAtom); - const toggleDrawer = useSetAtom(notificationDrawerExpandedAtom); - const navigate = useNavigate(); - const notifications = useAtomValue(notificationDrawerDataAtom); - const selectedNotifications = useAtomValue(notificationDrawerSelectedAtom); - const updateSelectedNotification = useSetAtom(updateNotificationSelectedAtom); const auth = useContext(ChromeAuthContext); const isOrgAdmin = auth?.user?.identity?.user?.is_org_admin; const { getUserPermissions } = useContext(InternalChromeContext); - const [hasNotificationsPermissions, setHasNotificationsPermissions] = useState(false); - const updateNotificationRead = useSetAtom(updateNotificationReadAtom); - const updateAllNotificationsSelected = useSetAtom(updateNotificationsSelectedAtom); - const [filterConfig, setFilterConfig] = useState([]); - - useEffect(() => { - let mounted = true; - const fetchPermissions = async () => { - const permissions = await getUserPermissions?.('notifications'); - if (mounted) { - setHasNotificationsPermissions( - permissions?.some((item) => - ['notifications:*:*', 'notifications:notifications:read', 'notifications:notifications:write'].includes( - (typeof item === 'string' && item) || item?.permission - ) - ) - ); - } - }; - const fetchFilterConfig = async () => { - try { - const response = await axios.get('/api/notifications/v1/notifications/facets/bundles'); - if (mounted) { - setFilterConfig( - response.data.map((bundle: Bundle) => ({ - title: bundle.displayName, - value: bundle.name, - })) - ); - } - } catch (error) { - console.error('Failed to fetch filter configuration:', error); - } - }; - fetchPermissions(); - fetchFilterConfig(); - return () => { - mounted = false; - }; - }, []); - - const filteredNotifications = useMemo( - () => - (activeFilters || []).reduce( - (acc: NotificationData[], chosenFilter: string) => [...acc, ...notifications.filter(({ bundle }) => bundle === chosenFilter)], - [] - ), - [activeFilters] - ); - - const onNotificationsDrawerClose = () => { - setActiveFilters([]); - toggleDrawer(false); - }; - - const onUpdateSelectedStatus = (read: boolean) => { - axios - .put('/api/notifications/v1/notifications/drawer/read', { - notification_ids: selectedNotifications.map((notification) => notification.id), - read_status: read, - }) - .then(() => { - selectedNotifications.forEach((notification) => updateNotificationRead(notification.id, read)); - setIsDropdownOpen(false); - updateAllNotificationsSelected(false); - }) - .catch((e) => { - console.error('failed to update notification read status', e); - }); - }; - - const selectAllNotifications = (selected: boolean) => { - updateAllNotificationsSelected(selected); - }; - - const selectVisibleNotifications = () => { - const visibleNotifications = activeFilters.length > 0 ? filteredNotifications : notifications; - visibleNotifications.forEach((notification) => updateSelectedNotification(notification.id, true)); - }; - const onFilterSelect = (chosenFilter: string) => { - activeFilters.includes(chosenFilter) - ? setActiveFilters(activeFilters.filter((filter) => filter !== chosenFilter)) - : setActiveFilters([...activeFilters, chosenFilter]); + const notificationProps = { + isOrgAdmin: isOrgAdmin, + getUserPermissions: getUserPermissions, + panelRef: panelRef, + expanded: true, + // toggle: toggle, }; - - const onNavigateTo = (link: string) => { - navigate(link); - onNotificationsDrawerClose(); - }; - - const dropdownItems = [ - , - { - onUpdateSelectedStatus(true); - }} - isDisabled={notifications.length === 0} - > - Mark selected as read - , - { - onUpdateSelectedStatus(false); - }} - isDisabled={notifications.length === 0} - > - Mark selected as unread - , - , - , - onNavigateTo('/settings/notifications/notificationslog')}> - - View notifications log - - , - (isOrgAdmin || hasNotificationsPermissions) && ( - onNavigateTo('/settings/notifications/configure-events')}> - - Configure notification settings - - - ), - onNavigateTo('/settings/notifications/user-preferences')}> - - Manage my notification preferences - - , - ]; - - const filterDropdownItems = () => { - return [ - - - {filterConfig.map((source) => ( - onFilterSelect(source.value)} - isDisabled={notifications.length === 0} - isSelected={activeFilters.includes(source.value)} - hasCheckbox - > - {source.title} - - ))} - - setActiveFilters([])}> - Reset filters - - - , - ]; - }; - - const renderNotifications = () => { - if (notifications.length === 0) { - return ; - } - - const sortedNotifications = orderBy(activeFilters?.length > 0 ? filteredNotifications : notifications, ['read', 'created'], ['asc', 'asc']); - - return sortedNotifications.map((notification) => ( - - )); + const PanelContent = () => { + return ( + } {...notificationProps} /> + ); }; return ( - - - {activeFilters.length > 0 && {activeFilters.length}} - ) => ( - setIsFilterDropdownOpen(!isFilterDropdownOpen)} - id="notifications-filter-toggle" - variant="plain" - aria-label="Notifications filter" - > - - - )} - isOpen={isFilterDropdownOpen} - onOpenChange={setIsFilterDropdownOpen} - popperProps={{ - position: PopoverPosition.right, - }} - id="notifications-filter-dropdown" - > - {filterDropdownItems()} - - selectAllNotifications(false) }, - { - title: `Select visible (${activeFilters.length > 0 ? filteredNotifications.length : notifications.length})`, - key: 'select-visible', - onClick: selectVisibleNotifications, - }, - { title: `Select all (${notifications.length})`, key: 'select-all', onClick: () => selectAllNotifications(true) }, - ]} - count={notifications.filter(({ selected }) => selected).length} - checked={notifications.length > 0 && notifications.every(({ selected }) => selected)} - /> - ) => ( - setIsDropdownOpen(!isDropdownOpen)} - variant="plain" - id="notifications-actions-toggle" - aria-label="Notifications actions dropdown" - isFullWidth - > - - - )} - isOpen={isDropdownOpen} - onOpenChange={setIsDropdownOpen} - popperProps={{ - position: PopoverPosition.right, - }} - id="notifications-actions-dropdown" - > - {dropdownItems} - - - - {renderNotifications()} - + + ); }; -const DrawerPanel = React.forwardRef((props, innerRef) => ); +const DrawerPanel = React.forwardRef((props, innerRef) => ); export default DrawerPanel; diff --git a/src/components/RootApp/ScalprumRoot.tsx b/src/components/RootApp/ScalprumRoot.tsx index 764d2b5ff..29b40c1c7 100644 --- a/src/components/RootApp/ScalprumRoot.tsx +++ b/src/components/RootApp/ScalprumRoot.tsx @@ -1,5 +1,4 @@ import React, { Suspense, lazy, memo, useCallback, useContext, useEffect, useMemo, useRef } from 'react'; -import axios from 'axios'; import { ScalprumProvider, ScalprumProviderProps } from '@scalprum/react-core'; import { shallowEqual, useSelector, useStore } from 'react-redux'; import { Route, Routes } from 'react-router-dom'; @@ -25,14 +24,13 @@ import ChromeFooter from '../Footer/Footer'; import updateSharedScope from '../../chrome/update-shared-scope'; import useBundleVisitDetection from '../../hooks/useBundleVisitDetection'; import chromeApiWrapper from './chromeApiWrapper'; -import { ITLess, getSevenDaysAgo } from '../../utils/common'; +import { ITLess } from '../../utils/common'; import InternalChromeContext from '../../utils/internalChromeContext'; import useChromeServiceEvents from '../../hooks/useChromeServiceEvents'; import useTrackPendoUsage from '../../hooks/useTrackPendoUsage'; import ChromeAuthContext from '../../auth/ChromeAuthContext'; import { onRegisterModuleWriteAtom } from '../../state/atoms/chromeModuleAtom'; import useTabName from '../../hooks/useTabName'; -import { NotificationData, notificationDrawerDataAtom } from '../../state/atoms/notificationDrawerAtom'; import { isPreviewAtom } from '../../state/atoms/releaseAtom'; import { addNavListenerAtom, deleteNavListenerAtom } from '../../state/atoms/activeAppAtom'; import BetaSwitcher from '../BetaSwitcher'; @@ -123,28 +121,9 @@ const ChromeApiRoot = ({ config, helpTopicsAPI, quickstartsAPI }: ChromeApiRootP // setting default tab title useTabName(); - const populateNotifications = useSetAtom(notificationDrawerDataAtom); - - async function getNotifications() { - try { - const { data } = await axios.get<{ data: NotificationData[] }>(`/api/notifications/v1/notifications/drawer`, { - params: { - limit: 50, - sort_by: 'read:asc', - startDate: getSevenDaysAgo(), - }, - }); - populateNotifications(data?.data || []); - } catch (error) { - console.error('Unable to get Notifications ', error); - } - } - useEffect(() => { // prepare webpack module sharing scope overrides updateSharedScope(); - // get notifications drawer api - getNotifications(); const unregister = chromeHistory.listen(historyListener); return () => { if (typeof unregister === 'function') { diff --git a/src/hooks/useChromeServiceEvents.ts b/src/hooks/useChromeServiceEvents.ts index 2bf11dc9a..3c0febfa0 100644 --- a/src/hooks/useChromeServiceEvents.ts +++ b/src/hooks/useChromeServiceEvents.ts @@ -19,7 +19,7 @@ type WsEventListenersRegistry = { [type in ChromeWsEventTypes]: Map>; }; -// needs to be outside rendring cycle to preserver clients when chrome API changes +// needs to be outside rendering cycle to preserver clients when chrome API changes const wsEventListenersRegistry: WsEventListenersRegistry = { [NOTIFICATION_DRAWER]: new Map(), }; diff --git a/src/layouts/DefaultLayout.tsx b/src/layouts/DefaultLayout.tsx index 8d373031d..1d86e8895 100644 --- a/src/layouts/DefaultLayout.tsx +++ b/src/layouts/DefaultLayout.tsx @@ -1,5 +1,4 @@ import React, { memo, useContext, useEffect, useRef, useState } from 'react'; -import { useAtomValue } from 'jotai'; import classnames from 'classnames'; import GlobalFilter from '../components/GlobalFilter/GlobalFilter'; import { useScalprum } from '@scalprum/react-core'; @@ -13,13 +12,12 @@ import isEqual from 'lodash/isEqual'; import ChromeRoutes from '../components/Routes/Routes'; import useOuiaTags from '../utils/useOuiaTags'; import RedirectBanner from '../components/Stratosphere/RedirectBanner'; +import { useAtomValue } from 'jotai'; import { useIntl } from 'react-intl'; import messages from '../locales/Messages'; import { CROSS_ACCESS_ACCOUNT_NUMBER } from '../utils/consts'; -import DrawerPanel from '../components/NotificationsDrawer/DrawerPanelContent'; - import '../components/Navigation/Navigation.scss'; import './DefaultLayout.scss'; import useNavigation from '../utils/useNavigation'; @@ -30,6 +28,7 @@ import ChromeAuthContext from '../auth/ChromeAuthContext'; import VirtualAssistant from '../components/Routes/VirtualAssistant'; import { notificationDrawerExpandedAtom } from '../state/atoms/notificationDrawerAtom'; import { ITLess } from '../utils/common'; +import DrawerPanel from '../components/NotificationsDrawer/DrawerPanelContent'; type ShieldedRootProps = { hideNav?: boolean; @@ -49,22 +48,35 @@ type DefaultLayoutProps = { }; const DefaultLayout: React.FC = ({ hasBanner, selectedAccountNumber, hideNav, isNavOpen, setIsNavOpen, Sidebar, Footer }) => { + const drawerPanelRef = useRef(null); + // const toggleDrawer = useSetAtom(notificationDrawerExpandedAtom); + useEffect(() => { + if (drawerPanelRef.current !== null) { + focusDrawer(); + } + }, []); + const focusDrawer = () => { + if (drawerPanelRef.current === null) { + return; + } + const tabbableElement = drawerPanelRef.current?.querySelector('[aria-label="Close"], a, button') as HTMLAnchorElement | HTMLButtonElement; + console.log(tabbableElement); + if (tabbableElement) { + tabbableElement.focus(); + } + }; const intl = useIntl(); const { loaded, schema, noNav } = useNavigation(); const isNotificationsDrawerExpanded = useAtomValue(notificationDrawerExpandedAtom); - const drawerPanelRef = useRef(); - const focusDrawer = () => { - const tabbableElement = drawerPanelRef.current?.querySelector('a, button') as HTMLAnchorElement | HTMLButtonElement; - tabbableElement.focus(); - }; const isNotificationsEnabled = useFlag('platform.chrome.notifications-drawer'); + return (
Date: Fri, 17 Jan 2025 12:43:06 -0500 Subject: [PATCH 2/2] Made toggleDrawer() function and passed it notifications drawer --- .../NotificationsDrawer/DrawerPanelContent.tsx | 14 +++++++------- src/layouts/DefaultLayout.tsx | 10 ++++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/components/NotificationsDrawer/DrawerPanelContent.tsx b/src/components/NotificationsDrawer/DrawerPanelContent.tsx index 3d2ad449b..298683ed5 100644 --- a/src/components/NotificationsDrawer/DrawerPanelContent.tsx +++ b/src/components/NotificationsDrawer/DrawerPanelContent.tsx @@ -9,12 +9,10 @@ import { ScalprumComponent } from '@scalprum/react-core'; export type DrawerPanelProps = { panelRef: React.Ref; - // toggle: () => void; + toggleDrawer: () => void; }; -const DrawerPanelBase = ({ panelRef }: DrawerPanelProps) => { - // toggle drawer will be an api or prop - +const DrawerPanelBase: React.FC = ({ panelRef, toggleDrawer }) => { const auth = useContext(ChromeAuthContext); const isOrgAdmin = auth?.user?.identity?.user?.is_org_admin; const { getUserPermissions } = useContext(InternalChromeContext); @@ -23,9 +21,9 @@ const DrawerPanelBase = ({ panelRef }: DrawerPanelProps) => { isOrgAdmin: isOrgAdmin, getUserPermissions: getUserPermissions, panelRef: panelRef, - expanded: true, - // toggle: toggle, + toggleDrawer: toggleDrawer, }; + const PanelContent = () => { return ( } {...notificationProps} /> @@ -39,6 +37,8 @@ const DrawerPanelBase = ({ panelRef }: DrawerPanelProps) => { ); }; -const DrawerPanel = React.forwardRef((props, innerRef) => ); +const DrawerPanel = React.forwardRef>((props, innerRef) => ( + +)); export default DrawerPanel; diff --git a/src/layouts/DefaultLayout.tsx b/src/layouts/DefaultLayout.tsx index 1d86e8895..6edb63750 100644 --- a/src/layouts/DefaultLayout.tsx +++ b/src/layouts/DefaultLayout.tsx @@ -12,7 +12,7 @@ import isEqual from 'lodash/isEqual'; import ChromeRoutes from '../components/Routes/Routes'; import useOuiaTags from '../utils/useOuiaTags'; import RedirectBanner from '../components/Stratosphere/RedirectBanner'; -import { useAtomValue } from 'jotai'; +import { useAtom } from 'jotai'; import { useIntl } from 'react-intl'; import messages from '../locales/Messages'; @@ -49,7 +49,6 @@ type DefaultLayoutProps = { const DefaultLayout: React.FC = ({ hasBanner, selectedAccountNumber, hideNav, isNavOpen, setIsNavOpen, Sidebar, Footer }) => { const drawerPanelRef = useRef(null); - // const toggleDrawer = useSetAtom(notificationDrawerExpandedAtom); useEffect(() => { if (drawerPanelRef.current !== null) { focusDrawer(); @@ -65,9 +64,12 @@ const DefaultLayout: React.FC = ({ hasBanner, selectedAccoun tabbableElement.focus(); } }; + const toggleDrawer = () => { + setIsNotificationsDrawerExpanded((prev) => !prev); + }; const intl = useIntl(); const { loaded, schema, noNav } = useNavigation(); - const isNotificationsDrawerExpanded = useAtomValue(notificationDrawerExpandedAtom); + const [isNotificationsDrawerExpanded, setIsNotificationsDrawerExpanded] = useAtom(notificationDrawerExpandedAtom); const isNotificationsEnabled = useFlag('platform.chrome.notifications-drawer'); return ( @@ -90,7 +92,7 @@ const DefaultLayout: React.FC = ({ hasBanner, selectedAccoun } {...(isNotificationsEnabled && { onNotificationDrawerExpand: focusDrawer, - notificationDrawer: , + notificationDrawer: , isNotificationDrawerExpanded: isNotificationsDrawerExpanded, })} sidebar={