Skip to content

Commit 1e82c7f

Browse files
BlakeHolifieldBlake HolifieldjustinorringerflorkbrHyperkid123
authored
[RHCLOUD-33811] Move notification drawer to its own module (#661)
* working prototype * working base; missing refs * modify panelRef * Passing toggle down and using it onClose() * webpack/cypress files * Testing stuff * mock useChrome function (not working) * Fix component tests * Prettier fixes * disable disallow-fec-relative-imports eslint * Cypress cleanup * Remove cypress configuration (drafted #667) * Moving test data to __test__ * Extra function body suggestion * Various formatting changes * EmptyNotifications to its own file * Swapping jotai to redux * Drafted singleton class * Replacing nested function * Changed to generic getDateDaysAgo * Uninstalling jotai * Moved dropdown components to a different file * Accidentally removed existing cypress import * WIP module nonsense * Debugging shared scope obj * Get unreadnotif * Finally working with JSclients and exposing var in scope and context as fed mod * Its working * Removing context provider * Using JS Client * Moving bell to notifications * Exposing DrawerBell * Removing unused test file --------- Co-authored-by: Blake Holifield <[email protected]> Co-authored-by: Justin Orringer <[email protected]> Co-authored-by: Bryan Florkiewicz <[email protected]> Co-authored-by: Martin Marosi <[email protected]>
1 parent c670ba0 commit 1e82c7f

16 files changed

+2454
-190
lines changed

fec.config.js

+8
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ module.exports = {
4949
__dirname,
5050
'./src/pages/Integrations/Create/IntegrationWizard.tsx'
5151
),
52+
'./initNotificationScope': path.resolve(
53+
__dirname,
54+
'./src/components/NotificationsDrawer/initNotificationScope.tsx'
55+
),
56+
'./NotificationsDrawerBell': path.resolve(
57+
__dirname,
58+
'./src/components/NotificationsDrawer/DrawerBell.tsx'
59+
),
5260
},
5361
exclude: ['react-router-dom'],
5462
shared: [

package-lock.json

+1,458-189
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@
1111
"@patternfly/react-icons": "^5.0.0",
1212
"@patternfly/react-styles": "^5.0.0",
1313
"@patternfly/react-table": "^5.0.0",
14-
"@redhat-cloud-services/frontend-components": "^4.0.1",
14+
"@redhat-cloud-services/frontend-components": "^4.2.22",
1515
"@redhat-cloud-services/frontend-components-notifications": "^4.0.0",
1616
"@redhat-cloud-services/frontend-components-translations": "^3.2.7",
1717
"@redhat-cloud-services/frontend-components-utilities": "^4.0.0",
1818
"@redhat-cloud-services/insights-common-typescript": "6.1.1",
1919
"@redhat-cloud-services/rbac-client": "1.2.2",
20+
"@redhat-cloud-services/types": "^1.0.14",
2021
"@unleash/proxy-client-react": "3.6.0",
2122
"axios": "0.27.2",
2223
"camelcase": "5.3.1",
@@ -36,12 +37,14 @@
3637
"react-dom": "^18.0.0",
3738
"react-fetching-library": "1.7.6",
3839
"react-intl": "6.6.2",
40+
"react-markdown": "^9.0.1",
3941
"react-redux": "7.2.9",
4042
"react-router-dom": "6.16.0",
4143
"react-string-format": "0.1.0",
4244
"react-use": "17.4.3",
4345
"redux-logger": "3.0.6",
4446
"redux-promise-middleware": "5.1.1",
47+
"remark-gfm": "^4.0.0",
4548
"timezones.json": "1.7.0",
4649
"typesafe-actions": "5.1.0",
4750
"typestyle": "2.1.0",
@@ -62,6 +65,7 @@
6265
"@redhat-cloud-services/tsc-transform-imports": "^1.0.8",
6366
"@redhat-cloud-services/integrations-client": "^3.2.0",
6467
"@redhat-cloud-services/notifications-client": "^2.5.3",
68+
"@scalprum/react-core": "^0.9.3",
6569
"@swc/jest": "^0.2.31",
6670
"@testing-library/dom": "9.3.4",
6771
"@testing-library/jest-dom": "6.3.0",

src/api/api.js

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
// Notifications endpoints
99
import getBundleFacets from '@redhat-cloud-services/notifications-client/dist/NotificationResourceV1GetBundleFacets';
1010
import getEventTypes from '@redhat-cloud-services/notifications-client/dist/NotificationResourceV1GetEventTypes';
11+
import getDrawerEntries from '@redhat-cloud-services/notifications-client/dist/DrawerResourceV1GetDrawerEntries';
12+
import updateNotificationReadStatus from '@redhat-cloud-services/notifications-client/dist/DrawerResourceV1UpdateNotificationReadStatus';
1113

1214
// Integrations endpoints
1315
import createEndpoint from '@redhat-cloud-services/integrations-client/dist/EndpointResourceV1CreateEndpoint';
@@ -34,6 +36,8 @@ const notificationsApi = new APIFactory(
3436
getBundleFacets,
3537
getEventTypes,
3638
getTimePreference,
39+
getDrawerEntries,
40+
updateNotificationReadStatus,
3741
putTimePreference,
3842
},
3943
{ axios: axiosInstance }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { getNotificationsApi } from '../../api';
2+
3+
const notificationsApi = getNotificationsApi();
4+
5+
export async function getDrawerEntries(config) {
6+
return await notificationsApi.getDrawerEntries(config);
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { getNotificationsApi } from '../../api';
2+
3+
const notificationsApi = getNotificationsApi();
4+
5+
export async function updateNotificationReadStatus(config) {
6+
return await notificationsApi.updateNotificationReadStatus(config);
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/* eslint-disable @typescript-eslint/ban-ts-comment */
2+
import React from 'react';
3+
import { NotificationBadge } from '@patternfly/react-core/dist/dynamic/components/NotificationBadge';
4+
import { ToolbarItem } from '@patternfly/react-core/dist/dynamic/components/Toolbar';
5+
import { Tooltip } from '@patternfly/react-core/dist/dynamic/components/Tooltip';
6+
import BellIcon from '@patternfly/react-icons/dist/dynamic/icons/bell-icon';
7+
import { DrawerSingleton } from './DrawerSingleton';
8+
9+
interface DrawerBellProps {
10+
isNotificationDrawerExpanded: boolean;
11+
toggleDrawer: () => void;
12+
}
13+
14+
const DrawerBell: React.ComponentType<DrawerBellProps> = ({
15+
isNotificationDrawerExpanded,
16+
toggleDrawer,
17+
}) => {
18+
return (
19+
<ToolbarItem className="pf-v6-u-mx-0">
20+
<Tooltip
21+
aria="none"
22+
aria-live="polite"
23+
content={'Notifications'}
24+
flipBehavior={['bottom']}
25+
className="tooltip-inner-settings-cy"
26+
>
27+
<NotificationBadge
28+
className="chr-c-notification-badge"
29+
variant={
30+
DrawerSingleton.Instance.hasUnreadNotifications()
31+
? 'unread'
32+
: 'read'
33+
}
34+
onClick={() => toggleDrawer()}
35+
aria-label="Notifications"
36+
isExpanded={isNotificationDrawerExpanded}
37+
>
38+
<BellIcon />
39+
</NotificationBadge>
40+
</Tooltip>
41+
</ToolbarItem>
42+
);
43+
};
44+
45+
export default DrawerBell;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
2+
import {
3+
ChromeWsEventTypes,
4+
ChromeWsPayload,
5+
} from '@redhat-cloud-services/types';
6+
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
7+
import BulkSelect from '@redhat-cloud-services/frontend-components/BulkSelect';
8+
9+
import {
10+
NotificationDrawerBody,
11+
NotificationDrawerHeader,
12+
NotificationDrawerList,
13+
} from '@patternfly/react-core/dist/dynamic/components/NotificationDrawer';
14+
import { Badge } from '@patternfly/react-core/dist/dynamic/components/Badge';
15+
16+
import orderBy from 'lodash/orderBy';
17+
import { useNavigate } from 'react-router-dom';
18+
import NotificationItem from './NotificationItem';
19+
import { EmptyNotifications } from './EmptyNotifications';
20+
import { NotificationData } from '../../types/Drawer';
21+
import useNotificationDrawer from '../../hooks/useNotificationDrawer';
22+
import { ActionDropdown, FilterDropdown } from './Dropdowns';
23+
24+
export type DrawerPanelProps = {
25+
panelRef: React.Ref<unknown>;
26+
isOrgAdmin: boolean;
27+
toggleDrawer: () => void;
28+
};
29+
30+
const DrawerPanelBase = ({ isOrgAdmin, toggleDrawer }: DrawerPanelProps) => {
31+
const { addWsEventListener } = useChrome();
32+
33+
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
34+
const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false);
35+
const {
36+
state,
37+
addNotification,
38+
updateNotificationRead,
39+
updateSelectedStatus,
40+
updateNotificationsSelected,
41+
updateNotificationSelected,
42+
setFilters,
43+
} = useNotificationDrawer();
44+
const navigate = useNavigate();
45+
46+
const eventType: ChromeWsEventTypes =
47+
'com.redhat.console.notifications.drawer';
48+
49+
const handleWsEvent = useCallback(
50+
(event: ChromeWsPayload<NotificationData>) => {
51+
addNotification(event.data as NotificationData);
52+
},
53+
[addNotification]
54+
);
55+
56+
useEffect(() => {
57+
const unregister = addWsEventListener(eventType, handleWsEvent);
58+
return () => {
59+
unregister();
60+
};
61+
}, [addWsEventListener, handleWsEvent]);
62+
63+
const filteredNotifications = useMemo(() => {
64+
const notificationsByBundle = state.notificationData.reduce(
65+
(acc, notification) => {
66+
if (!acc[notification.bundle]) {
67+
acc[notification.bundle] = [];
68+
}
69+
acc[notification.bundle].push(notification);
70+
return acc;
71+
},
72+
{}
73+
);
74+
75+
return (state.filters || []).reduce((acc, chosenFilter) => {
76+
if (notificationsByBundle[chosenFilter]) {
77+
acc.push(...notificationsByBundle[chosenFilter]);
78+
}
79+
return acc;
80+
}, [] as NotificationData[]);
81+
}, [state.filters, state.notificationData]);
82+
83+
const onNotificationsDrawerClose = () => {
84+
setFilters([]);
85+
toggleDrawer();
86+
};
87+
88+
const onUpdateSelectedStatus = (read: boolean) => {
89+
updateSelectedStatus(read);
90+
setIsDropdownOpen(false);
91+
};
92+
93+
const selectAllNotifications = (selected: boolean) => {
94+
updateNotificationsSelected(selected);
95+
};
96+
97+
const selectVisibleNotifications = () => {
98+
const visibleNotifications =
99+
state.filters.length > 0 ? filteredNotifications : state.notificationData;
100+
visibleNotifications.forEach((notification) =>
101+
updateNotificationSelected(notification.id, true)
102+
);
103+
};
104+
105+
const onFilterSelect = (chosenFilter: string) => {
106+
state.filters.includes(chosenFilter)
107+
? setFilters(state.filters.filter((filter) => filter !== chosenFilter))
108+
: setFilters([...state.filters, chosenFilter]);
109+
};
110+
111+
const onNavigateTo = (link: string) => {
112+
navigate(link);
113+
onNotificationsDrawerClose();
114+
};
115+
116+
const toggleDropdown = () => setIsDropdownOpen(!isDropdownOpen);
117+
118+
const RenderNotifications = () => {
119+
if (state.notificationData.length === 0) {
120+
return (
121+
<EmptyNotifications
122+
isOrgAdmin={isOrgAdmin}
123+
onLinkClick={onNotificationsDrawerClose}
124+
/>
125+
);
126+
}
127+
128+
const sortedNotifications = orderBy(
129+
state.filters?.length > 0
130+
? filteredNotifications
131+
: state.notificationData,
132+
['read', 'created'],
133+
['asc', 'asc']
134+
);
135+
136+
return sortedNotifications.map((notification) => (
137+
<NotificationItem
138+
key={notification.id}
139+
notification={notification}
140+
onNavigateTo={onNavigateTo}
141+
updateNotificationSelected={updateNotificationSelected}
142+
updateNotificationRead={updateNotificationRead}
143+
/>
144+
));
145+
};
146+
147+
return (
148+
<>
149+
<NotificationDrawerHeader
150+
onClose={onNotificationsDrawerClose}
151+
title="Notifications"
152+
className="pf-u-align-items-center"
153+
>
154+
{state.filters.length > 0 && (
155+
<Badge isRead>{state.filters.length}</Badge>
156+
)}
157+
<FilterDropdown
158+
filterConfig={state.filterConfig}
159+
isDisabled={state.notificationData.length === 0}
160+
activeFilters={state.filters}
161+
setActiveFilters={setFilters}
162+
onFilterSelect={onFilterSelect}
163+
isFilterDropdownOpen={isFilterDropdownOpen}
164+
setIsFilterDropdownOpen={setIsFilterDropdownOpen}
165+
/>
166+
<BulkSelect
167+
id="notifications-bulk-select"
168+
items={[
169+
{
170+
title: 'Select none (0)',
171+
key: 'select-none',
172+
onClick: () => selectAllNotifications(false),
173+
},
174+
{
175+
title: `Select visible (${
176+
state.filters.length > 0
177+
? filteredNotifications.length
178+
: state.notificationData.length
179+
})`,
180+
key: 'select-visible',
181+
onClick: selectVisibleNotifications,
182+
},
183+
{
184+
title: `Select all (${state.notificationData.length})`,
185+
key: 'select-all',
186+
onClick: () => selectAllNotifications(true),
187+
},
188+
]}
189+
count={
190+
state.notificationData.filter(({ selected }) => selected).length
191+
}
192+
checked={
193+
state.notificationData.length > 0 &&
194+
state.notificationData.every(({ selected }) => selected)
195+
}
196+
/>
197+
<ActionDropdown
198+
isDropdownOpen={isDropdownOpen}
199+
setIsDropdownOpen={toggleDropdown}
200+
isDisabled={state.notificationData.length === 0}
201+
onUpdateSelectedStatus={onUpdateSelectedStatus}
202+
onNavigateTo={onNavigateTo}
203+
isOrgAdmin={isOrgAdmin}
204+
hasNotificationsPermissions={state.hasNotificationsPermissions}
205+
/>
206+
</NotificationDrawerHeader>
207+
<NotificationDrawerBody>
208+
<NotificationDrawerList>
209+
<RenderNotifications />
210+
</NotificationDrawerList>
211+
</NotificationDrawerBody>
212+
</>
213+
);
214+
};
215+
216+
const DrawerPanel = React.forwardRef(
217+
(props: DrawerPanelProps, panelRef: React.Ref<unknown>) => (
218+
<DrawerPanelBase
219+
isOrgAdmin={props.isOrgAdmin}
220+
panelRef={panelRef}
221+
toggleDrawer={props.toggleDrawer}
222+
/>
223+
)
224+
);
225+
DrawerPanel.displayName = 'DrawerPanel';
226+
227+
export default DrawerPanel;

0 commit comments

Comments
 (0)