Skip to content

Commit 4ddfe38

Browse files
authored
RI-7634 improve live notifications (#5116)
* refactor Notifications.tsx * refactor type of InfiniteMessages * refactor Notifications to only show single live notification at a time
1 parent 723c9a5 commit 4ddfe38

File tree

21 files changed

+495
-406
lines changed

21 files changed

+495
-406
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,8 @@ static/
8888
.env*
8989
.npmrc
9090

91+
# AI rules
92+
.windsurfrules
93+
.junie/
9194
*storybook.log
9295
storybook-static

redisinsight/ui/src/components/base/display/toast/RiToast.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ToastOptions as RcToastOptions } from 'react-toastify'
99

1010
import { CommonProps } from 'uiSrc/components/base/theme/types'
1111
import { ColorText, Text } from 'uiSrc/components/base/text'
12+
import { ColorType } from 'uiSrc/components/base/text/text.styles'
1213
import { Spacer } from '../../layout'
1314

1415
type RiToastProps = React.ComponentProps<typeof Toast>
@@ -27,18 +28,15 @@ export const riToast = (
2728
}
2829

2930
if (typeof message === 'string') {
30-
let color = options?.variant
31+
let color: ColorType = options?.variant
3132
if (color === 'informative') {
32-
// @ts-ignore
3333
color = 'subdued'
3434
}
3535
toastContent.message = (
36-
<ColorText color={color}>
37-
<Text size="M" variant="semiBold">
38-
{message}
39-
</Text>
36+
<Text size="M" variant="semiBold">
37+
<ColorText color={color}>{message}</ColorText>
4038
<Spacer size="s" />
41-
</ColorText>
39+
</Text>
4240
)
4341
} else {
4442
toastContent.message = message
@@ -55,3 +53,4 @@ export const riToast = (
5553
riToast.Variant = toast.Variant
5654
riToast.Position = toast.Position
5755
riToast.dismiss = toast.dismiss
56+
riToast.isActive = toast.isActive

redisinsight/ui/src/components/base/external-link/ExternalLink.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ const ExternalLink = (props: Props) => {
2222
} = props
2323

2424
const ArrowIcon = () => (
25-
<RiIcon type="ArrowDiagonalIcon" size={iconSize} color="informative400" />
25+
<RiIcon
26+
type="ArrowDiagonalIcon"
27+
size={iconSize || size}
28+
color="informative400"
29+
/>
2630
)
2731

2832
return (

redisinsight/ui/src/components/base/text/text.styles.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type EuiColorNames =
1414
| 'accent'
1515
| 'warning'
1616
| 'success'
17+
1718
export type ColorType = BodyProps['color'] | EuiColorNames | (string & {})
1819
export interface MapProps extends HTMLAttributes<HTMLElement> {
1920
$color?: ColorType
Lines changed: 13 additions & 224 deletions
Original file line numberDiff line numberDiff line change
@@ -1,230 +1,19 @@
1-
import React, { useEffect, useRef } from 'react'
2-
import { useDispatch, useSelector } from 'react-redux'
3-
import cx from 'classnames'
4-
import {
5-
errorsSelector,
6-
infiniteNotificationsSelector,
7-
messagesSelector,
8-
removeInfiniteNotification,
9-
removeMessage,
10-
} from 'uiSrc/slices/app/notifications'
11-
import { setReleaseNotesViewed } from 'uiSrc/slices/app/info'
12-
import { IError, IMessage, InfiniteMessage } from 'uiSrc/slices/interfaces'
13-
import { ApiEncryptionErrors } from 'uiSrc/constants/apiErrors'
14-
import { DEFAULT_ERROR_MESSAGE } from 'uiSrc/utils'
15-
import { showOAuthProgress } from 'uiSrc/slices/oauth/cloud'
16-
import { CustomErrorCodes } from 'uiSrc/constants'
17-
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
18-
import { ColorText } from 'uiSrc/components/base/text'
19-
import { riToast, RiToaster } from 'uiSrc/components/base/display/toast'
20-
21-
import errorMessages from './error-messages'
22-
import { InfiniteMessagesIds } from './components'
23-
24-
import styles from './styles.module.scss'
25-
26-
const ONE_HOUR = 3_600_000
27-
const DEFAULT_ERROR_TITLE = 'Error'
1+
import React from 'react'
2+
import { RiToaster } from 'uiSrc/components/base/display/toast'
3+
import { useErrorNotifications, useMessageNotifications } from './hooks'
4+
import { InfiniteNotifications } from './components/infinite-messages/InfiniteNotifications'
5+
import { defaultContainerId } from './constants'
286

297
const Notifications = () => {
30-
const messagesData = useSelector(messagesSelector)
31-
const errorsData = useSelector(errorsSelector)
32-
const infiniteNotifications = useSelector(infiniteNotificationsSelector)
33-
34-
const dispatch = useDispatch()
35-
const toastIdsRef = useRef(new Map())
36-
37-
const removeToast = (id: string) => {
38-
if (toastIdsRef.current.has(id)) {
39-
riToast.dismiss(toastIdsRef.current.get(id))
40-
toastIdsRef.current.delete(id)
41-
}
42-
dispatch(removeMessage(id))
43-
}
44-
45-
const onSubmitNotification = (id: string, group?: string) => {
46-
if (group === 'upgrade') {
47-
dispatch(setReleaseNotesViewed(true))
48-
}
49-
dispatch(removeMessage(id))
50-
}
51-
52-
const getSuccessText = (text: string | JSX.Element | JSX.Element[]) => (
53-
<ColorText color="success">{text}</ColorText>
8+
useErrorNotifications()
9+
useMessageNotifications()
10+
11+
return (
12+
<>
13+
<InfiniteNotifications />
14+
<RiToaster containerId={defaultContainerId} />
15+
</>
5416
)
55-
56-
const showSuccessToasts = (data: IMessage[]) =>
57-
data.forEach(
58-
({
59-
id = '',
60-
title = '',
61-
message = '',
62-
showCloseButton = true,
63-
actions,
64-
className,
65-
group,
66-
}) => {
67-
const handleClose = () => {
68-
onSubmitNotification(id, group)
69-
removeToast(id)
70-
}
71-
if (toastIdsRef.current.has(id)) {
72-
removeToast(id)
73-
return
74-
}
75-
const toastId = riToast(
76-
{
77-
className,
78-
message: title,
79-
description: getSuccessText(message),
80-
actions: actions ?? {
81-
primary: {
82-
closes: true,
83-
label: 'OK',
84-
onClick: handleClose,
85-
},
86-
},
87-
showCloseButton,
88-
},
89-
{ variant: riToast.Variant.Success, toastId: id },
90-
)
91-
toastIdsRef.current.set(id, toastId)
92-
},
93-
)
94-
95-
const showErrorsToasts = (errors: IError[]) =>
96-
errors.forEach(
97-
({
98-
id = '',
99-
message = DEFAULT_ERROR_MESSAGE,
100-
instanceId = '',
101-
name,
102-
title = DEFAULT_ERROR_TITLE,
103-
additionalInfo,
104-
}) => {
105-
if (toastIdsRef.current.has(id)) {
106-
removeToast(id)
107-
return
108-
}
109-
let toastId: ReturnType<typeof riToast>
110-
if (ApiEncryptionErrors.includes(name)) {
111-
toastId = errorMessages.ENCRYPTION(
112-
() => removeToast(id),
113-
instanceId,
114-
id,
115-
)
116-
} else if (
117-
additionalInfo?.errorCode ===
118-
CustomErrorCodes.CloudCapiKeyUnauthorized
119-
) {
120-
toastId = errorMessages.CLOUD_CAPI_KEY_UNAUTHORIZED(
121-
{ message, title },
122-
additionalInfo,
123-
() => removeToast(id),
124-
id,
125-
)
126-
} else if (
127-
additionalInfo?.errorCode ===
128-
CustomErrorCodes.RdiDeployPipelineFailure
129-
) {
130-
toastId = errorMessages.RDI_DEPLOY_PIPELINE(
131-
{ title, message },
132-
() => removeToast(id),
133-
id,
134-
)
135-
} else {
136-
toastId = errorMessages.DEFAULT(
137-
message,
138-
() => removeToast(id),
139-
title,
140-
id,
141-
)
142-
}
143-
144-
toastIdsRef.current.set(id, toastId)
145-
},
146-
)
147-
const infiniteToastIdsRef = useRef(new Set<number | string>())
148-
149-
const showInfiniteToasts = (data: InfiniteMessage[]) => {
150-
infiniteToastIdsRef.current.forEach((toastId) => {
151-
setTimeout(() => {
152-
riToast.dismiss(toastId)
153-
infiniteToastIdsRef.current.delete(toastId)
154-
}, 50)
155-
})
156-
data.forEach((notification: InfiniteMessage) => {
157-
const {
158-
id,
159-
message,
160-
description,
161-
actions,
162-
className = '',
163-
variant,
164-
customIcon,
165-
showCloseButton = true,
166-
onClose: onCloseCallback,
167-
} = notification
168-
const toastId = riToast(
169-
{
170-
className: cx(styles.infiniteMessage, className),
171-
message: message,
172-
description: description,
173-
actions,
174-
showCloseButton,
175-
customIcon,
176-
onClose: () => {
177-
switch (id) {
178-
case InfiniteMessagesIds.oAuthProgress:
179-
dispatch(showOAuthProgress(false))
180-
break
181-
case InfiniteMessagesIds.databaseExists:
182-
sendEventTelemetry({
183-
event:
184-
TelemetryEvent.CLOUD_IMPORT_EXISTING_DATABASE_FORM_CLOSED,
185-
})
186-
break
187-
case InfiniteMessagesIds.subscriptionExists:
188-
sendEventTelemetry({
189-
event:
190-
TelemetryEvent.CLOUD_CREATE_DATABASE_IN_SUBSCRIPTION_FORM_CLOSED,
191-
})
192-
break
193-
case InfiniteMessagesIds.appUpdateAvailable:
194-
sendEventTelemetry({
195-
event: TelemetryEvent.UPDATE_NOTIFICATION_CLOSED,
196-
})
197-
break
198-
default:
199-
break
200-
}
201-
202-
dispatch(removeInfiniteNotification(id))
203-
onCloseCallback?.()
204-
},
205-
},
206-
{
207-
variant: variant ?? riToast.Variant.Notice,
208-
autoClose: ONE_HOUR,
209-
toastId: id,
210-
},
211-
)
212-
infiniteToastIdsRef.current.add(toastId)
213-
toastIdsRef.current.set(id, toastId)
214-
})
215-
}
216-
217-
useEffect(() => {
218-
showSuccessToasts(messagesData)
219-
}, [messagesData])
220-
useEffect(() => {
221-
showErrorsToasts(errorsData)
222-
}, [errorsData])
223-
useEffect(() => {
224-
showInfiniteToasts(infiniteNotifications)
225-
}, [infiniteNotifications])
226-
227-
return <RiToaster />
22817
}
22918

23019
export default Notifications

redisinsight/ui/src/components/notifications/components/default-error-content/DefaultErrorContent.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import React from 'react'
22

33
import { ColorText } from 'uiSrc/components/base/text'
4-
import { Spacer } from 'uiSrc/components/base/layout/spacer'
5-
import { SecondaryButton } from 'uiSrc/components/base/forms/buttons'
64

75
export interface Props {
86
text: string | JSX.Element | JSX.Element[]

redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteMessages.tsx

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { FlexItem, Row } from 'uiSrc/components/base/layout/flex'
2020
import { Spacer } from 'uiSrc/components/base/layout/spacer'
2121
import { PrimaryButton } from 'uiSrc/components/base/forms/buttons'
2222
import { RiIcon } from 'uiSrc/components/base/icons/RiIcon'
23+
2324
import styles from './styles.module.scss'
2425

2526
export enum InfiniteMessagesIds {
@@ -38,11 +39,32 @@ const MANAGE_DB_LINK = getUtmExternalLink(EXTERNAL_LINKS.cloudConsole, {
3839
medium: UTM_MEDIUMS.Main,
3940
})
4041

41-
// TODO: Refactor this type definition to work with the real parameters and their types we use in each message
42-
export const INFINITE_MESSAGES: Record<
43-
string,
44-
(...args: any[]) => InfiniteMessage
45-
> = {
42+
interface InfiniteMessagesType {
43+
AUTHENTICATING: () => InfiniteMessage
44+
PENDING_CREATE_DB: (step?: CloudJobStep) => InfiniteMessage
45+
SUCCESS_CREATE_DB: (
46+
details: Omit<CloudSuccessResult, 'resourceId'>,
47+
onSuccess: () => void,
48+
jobName: Maybe<CloudJobName>,
49+
) => InfiniteMessage
50+
DATABASE_EXISTS: (
51+
onSuccess?: () => void,
52+
onClose?: () => void,
53+
) => InfiniteMessage
54+
DATABASE_IMPORT_FORBIDDEN: (onClose?: () => void) => InfiniteMessage
55+
SUBSCRIPTION_EXISTS: (
56+
onSuccess?: () => void,
57+
onClose?: () => void,
58+
) => InfiniteMessage
59+
AUTO_CREATING_DATABASE: () => InfiniteMessage
60+
APP_UPDATE_AVAILABLE: (
61+
version: string,
62+
onSuccess?: () => void,
63+
) => InfiniteMessage
64+
SUCCESS_DEPLOY_PIPELINE: () => InfiniteMessage
65+
}
66+
67+
export const INFINITE_MESSAGES: InfiniteMessagesType = {
4668
AUTHENTICATING: () => ({
4769
id: InfiniteMessagesIds.oAuthProgress,
4870
message: 'Authenticating…',
@@ -52,6 +74,7 @@ export const INFINITE_MESSAGES: Record<
5274
PENDING_CREATE_DB: (step?: CloudJobStep) => ({
5375
id: InfiniteMessagesIds.oAuthProgress,
5476
customIcon: LoaderLargeIcon,
77+
variation: step,
5578
message: (
5679
<>
5780
{(step === CloudJobStep.Credentials || !step) &&
@@ -207,8 +230,7 @@ export const INFINITE_MESSAGES: Record<
207230
}),
208231
SUBSCRIPTION_EXISTS: (onSuccess?: () => void, onClose?: () => void) => ({
209232
id: InfiniteMessagesIds.subscriptionExists,
210-
message:
211-
'Your subscription does not have a free Redis Cloud database.',
233+
message: 'Your subscription does not have a free Redis Cloud database.',
212234
description:
213235
'Do you want to create a free database in your existing subscription?',
214236
actions: {

0 commit comments

Comments
 (0)