From 67388bba1920fca1a40993b341b2a5a6361314a7 Mon Sep 17 00:00:00 2001 From: pd-redis Date: Thu, 16 Oct 2025 10:42:47 +0300 Subject: [PATCH 1/7] refactor Notifications.tsx --- .../base/external-link/ExternalLink.tsx | 6 +- .../notifications/Notifications.tsx | 231 +----------------- .../components/notifications/hooks/index.ts | 3 + .../hooks/useErrorNotifications.ts | 81 ++++++ .../hooks/useInfiniteNotifications.ts | 89 +++++++ .../hooks/useMessageNotifications.tsx | 73 ++++++ 6 files changed, 260 insertions(+), 223 deletions(-) create mode 100644 redisinsight/ui/src/components/notifications/hooks/index.ts create mode 100644 redisinsight/ui/src/components/notifications/hooks/useErrorNotifications.ts create mode 100644 redisinsight/ui/src/components/notifications/hooks/useInfiniteNotifications.ts create mode 100644 redisinsight/ui/src/components/notifications/hooks/useMessageNotifications.tsx diff --git a/redisinsight/ui/src/components/base/external-link/ExternalLink.tsx b/redisinsight/ui/src/components/base/external-link/ExternalLink.tsx index 0841ad56ca..5d0c2fa03e 100644 --- a/redisinsight/ui/src/components/base/external-link/ExternalLink.tsx +++ b/redisinsight/ui/src/components/base/external-link/ExternalLink.tsx @@ -22,7 +22,11 @@ const ExternalLink = (props: Props) => { } = props const ArrowIcon = () => ( - + ) return ( diff --git a/redisinsight/ui/src/components/notifications/Notifications.tsx b/redisinsight/ui/src/components/notifications/Notifications.tsx index a7c8d0be6f..d9be9f4f2b 100644 --- a/redisinsight/ui/src/components/notifications/Notifications.tsx +++ b/redisinsight/ui/src/components/notifications/Notifications.tsx @@ -1,228 +1,15 @@ -import React, { useEffect, useRef } from 'react' -import { useDispatch, useSelector } from 'react-redux' -import cx from 'classnames' +import React from 'react' +import { RiToaster } from 'uiSrc/components/base/display/toast' import { - errorsSelector, - infiniteNotificationsSelector, - messagesSelector, - removeInfiniteNotification, - removeMessage, -} from 'uiSrc/slices/app/notifications' -import { setReleaseNotesViewed } from 'uiSrc/slices/app/info' -import { IError, IMessage, InfiniteMessage } from 'uiSrc/slices/interfaces' -import { ApiEncryptionErrors } from 'uiSrc/constants/apiErrors' -import { DEFAULT_ERROR_MESSAGE } from 'uiSrc/utils' -import { showOAuthProgress } from 'uiSrc/slices/oauth/cloud' -import { CustomErrorCodes } from 'uiSrc/constants' -import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { ColorText } from 'uiSrc/components/base/text' -import { riToast, RiToaster } from 'uiSrc/components/base/display/toast' - -import errorMessages from './error-messages' -import { InfiniteMessagesIds } from './components' - -import styles from './styles.module.scss' - -const ONE_HOUR = 3_600_000 -const DEFAULT_ERROR_TITLE = 'Error' + useErrorNotifications, + useMessageNotifications, + useInfiniteNotifications, +} from './hooks' const Notifications = () => { - const messagesData = useSelector(messagesSelector) - const errorsData = useSelector(errorsSelector) - const infiniteNotifications = useSelector(infiniteNotificationsSelector) - - const dispatch = useDispatch() - const toastIdsRef = useRef(new Map()) - - const removeToast = (id: string) => { - if (toastIdsRef.current.has(id)) { - riToast.dismiss(toastIdsRef.current.get(id)) - toastIdsRef.current.delete(id) - } - dispatch(removeMessage(id)) - } - - const onSubmitNotification = (id: string, group?: string) => { - if (group === 'upgrade') { - dispatch(setReleaseNotesViewed(true)) - } - dispatch(removeMessage(id)) - } - - const getSuccessText = (text: string | JSX.Element | JSX.Element[]) => ( - {text} - ) - - const showSuccessToasts = (data: IMessage[]) => - data.forEach( - ({ - id = '', - title = '', - message = '', - showCloseButton = true, - actions, - className, - group, - }) => { - const handleClose = () => { - onSubmitNotification(id, group) - removeToast(id) - } - if (toastIdsRef.current.has(id)) { - removeToast(id) - return - } - const toastId = riToast( - { - className, - message: title, - description: getSuccessText(message), - actions: actions ?? { - primary: { - closes: true, - label: 'OK', - onClick: handleClose, - }, - }, - showCloseButton, - }, - { variant: riToast.Variant.Success, toastId: id }, - ) - toastIdsRef.current.set(id, toastId) - }, - ) - - const showErrorsToasts = (errors: IError[]) => - errors.forEach( - ({ - id = '', - message = DEFAULT_ERROR_MESSAGE, - instanceId = '', - name, - title = DEFAULT_ERROR_TITLE, - additionalInfo, - }) => { - if (toastIdsRef.current.has(id)) { - removeToast(id) - return - } - let toastId: ReturnType - if (ApiEncryptionErrors.includes(name)) { - toastId = errorMessages.ENCRYPTION( - () => removeToast(id), - instanceId, - id, - ) - } else if ( - additionalInfo?.errorCode === - CustomErrorCodes.CloudCapiKeyUnauthorized - ) { - toastId = errorMessages.CLOUD_CAPI_KEY_UNAUTHORIZED( - { message, title }, - additionalInfo, - () => removeToast(id), - id, - ) - } else if ( - additionalInfo?.errorCode === - CustomErrorCodes.RdiDeployPipelineFailure - ) { - toastId = errorMessages.RDI_DEPLOY_PIPELINE( - { title, message }, - () => removeToast(id), - id, - ) - } else { - toastId = errorMessages.DEFAULT( - message, - () => removeToast(id), - title, - id, - ) - } - - toastIdsRef.current.set(id, toastId) - }, - ) - const infiniteToastIdsRef = useRef(new Set()) - - const showInfiniteToasts = (data: InfiniteMessage[]) => { - infiniteToastIdsRef.current.forEach((toastId) => { - setTimeout(() => { - riToast.dismiss(toastId) - infiniteToastIdsRef.current.delete(toastId) - }, 50) - }) - data.forEach((notification: InfiniteMessage) => { - const { - id, - message, - description, - actions, - className = '', - variant, - customIcon, - showCloseButton = true, - onClose: onCloseCallback, - } = notification - const toastId = riToast( - { - className: cx(styles.infiniteMessage, className), - message: message, - description: description, - actions, - showCloseButton, - customIcon, - onClose: () => { - switch (id) { - case InfiniteMessagesIds.oAuthProgress: - dispatch(showOAuthProgress(false)) - break - case InfiniteMessagesIds.databaseExists: - sendEventTelemetry({ - event: - TelemetryEvent.CLOUD_IMPORT_EXISTING_DATABASE_FORM_CLOSED, - }) - break - case InfiniteMessagesIds.subscriptionExists: - sendEventTelemetry({ - event: - TelemetryEvent.CLOUD_CREATE_DATABASE_IN_SUBSCRIPTION_FORM_CLOSED, - }) - break - case InfiniteMessagesIds.appUpdateAvailable: - sendEventTelemetry({ - event: TelemetryEvent.UPDATE_NOTIFICATION_CLOSED, - }) - break - default: - break - } - - dispatch(removeInfiniteNotification(id)) - onCloseCallback?.() - }, - }, - { - variant: variant ?? riToast.Variant.Notice, - autoClose: ONE_HOUR, - toastId: id, - }, - ) - infiniteToastIdsRef.current.add(toastId) - toastIdsRef.current.set(id, toastId) - }) - } - - useEffect(() => { - showSuccessToasts(messagesData) - }, [messagesData]) - useEffect(() => { - showErrorsToasts(errorsData) - }, [errorsData]) - useEffect(() => { - showInfiniteToasts(infiniteNotifications) - }, [infiniteNotifications]) + useErrorNotifications() + useMessageNotifications() + useInfiniteNotifications() return } diff --git a/redisinsight/ui/src/components/notifications/hooks/index.ts b/redisinsight/ui/src/components/notifications/hooks/index.ts new file mode 100644 index 0000000000..d873241dec --- /dev/null +++ b/redisinsight/ui/src/components/notifications/hooks/index.ts @@ -0,0 +1,3 @@ +export { useErrorNotifications } from './useErrorNotifications' +export { useMessageNotifications } from './useMessageNotifications' +export { useInfiniteNotifications } from './useInfiniteNotifications' diff --git a/redisinsight/ui/src/components/notifications/hooks/useErrorNotifications.ts b/redisinsight/ui/src/components/notifications/hooks/useErrorNotifications.ts new file mode 100644 index 0000000000..7a837ff4d8 --- /dev/null +++ b/redisinsight/ui/src/components/notifications/hooks/useErrorNotifications.ts @@ -0,0 +1,81 @@ +import { useEffect, useRef } from 'react' +import { useDispatch, useSelector } from 'react-redux' + +import { IError } from 'uiSrc/slices/interfaces' +import { DEFAULT_ERROR_MESSAGE } from 'uiSrc/utils' +import { riToast } from 'uiSrc/components/base/display/toast' +import { ApiEncryptionErrors } from 'uiSrc/constants/apiErrors' +import errorMessages from 'uiSrc/components/notifications/error-messages' +import { CustomErrorCodes } from 'uiSrc/constants' +import { errorsSelector, removeMessage } from 'uiSrc/slices/app/notifications' + +const DEFAULT_ERROR_TITLE = 'Error' + +export const useErrorNotifications = () => { + const errorsData = useSelector(errorsSelector) + const dispatch = useDispatch() + const toastIdsRef = useRef(new Map()) + const removeToast = (id: string) => { + if (toastIdsRef.current.has(id)) { + riToast.dismiss(toastIdsRef.current.get(id)) + toastIdsRef.current.delete(id) + } + dispatch(removeMessage(id)) + } + const showErrorsToasts = (errors: IError[]) => + errors.forEach( + ({ + id = '', + message = DEFAULT_ERROR_MESSAGE, + instanceId = '', + name, + title = DEFAULT_ERROR_TITLE, + additionalInfo, + }) => { + if (toastIdsRef.current.has(id)) { + removeToast(id) + return + } + let toastId: ReturnType + if (ApiEncryptionErrors.includes(name)) { + toastId = errorMessages.ENCRYPTION( + () => removeToast(id), + instanceId, + id, + ) + } else if ( + additionalInfo?.errorCode === + CustomErrorCodes.CloudCapiKeyUnauthorized + ) { + toastId = errorMessages.CLOUD_CAPI_KEY_UNAUTHORIZED( + { message, title }, + additionalInfo, + () => removeToast(id), + id, + ) + } else if ( + additionalInfo?.errorCode === + CustomErrorCodes.RdiDeployPipelineFailure + ) { + toastId = errorMessages.RDI_DEPLOY_PIPELINE( + { title, message }, + () => removeToast(id), + id, + ) + } else { + toastId = errorMessages.DEFAULT( + message, + () => removeToast(id), + title, + id, + ) + } + + toastIdsRef.current.set(id, toastId) + }, + ) + + useEffect(() => { + showErrorsToasts(errorsData) + }, [errorsData]) +} diff --git a/redisinsight/ui/src/components/notifications/hooks/useInfiniteNotifications.ts b/redisinsight/ui/src/components/notifications/hooks/useInfiniteNotifications.ts new file mode 100644 index 0000000000..59fc01757a --- /dev/null +++ b/redisinsight/ui/src/components/notifications/hooks/useInfiniteNotifications.ts @@ -0,0 +1,89 @@ +import { useEffect, useRef } from 'react' +import { useDispatch, useSelector } from 'react-redux' + +import { InfiniteMessage } from 'uiSrc/slices/interfaces' +import { riToast } from 'uiSrc/components/base/display/toast' +import { + infiniteNotificationsSelector, + removeInfiniteNotification, +} from 'uiSrc/slices/app/notifications' +import cx from 'classnames' +import { InfiniteMessagesIds } from 'uiSrc/components/notifications/components' +import { showOAuthProgress } from 'uiSrc/slices/oauth/cloud' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' + +const ONE_HOUR = 3_600_000 + +export const useInfiniteNotifications = () => { + const infiniteNotifications = useSelector(infiniteNotificationsSelector) + const dispatch = useDispatch() + const infiniteToastIdsRef = useRef(new Map()) + + const showInfiniteToasts = (data: InfiniteMessage[]) => { + data.forEach((notification: InfiniteMessage) => { + const { + id, + message, + description, + actions, + className = '', + variant, + customIcon, + showCloseButton = true, + onClose: onCloseCallback, + } = notification + const toastId = riToast( + { + className: cx(className), + message: message, + description: description, + actions, + showCloseButton, + customIcon, + onClose: () => { + switch (id) { + case InfiniteMessagesIds.oAuthProgress: + dispatch(showOAuthProgress(false)) + break + case InfiniteMessagesIds.databaseExists: + sendEventTelemetry({ + event: + TelemetryEvent.CLOUD_IMPORT_EXISTING_DATABASE_FORM_CLOSED, + }) + break + case InfiniteMessagesIds.subscriptionExists: + sendEventTelemetry({ + event: + TelemetryEvent.CLOUD_CREATE_DATABASE_IN_SUBSCRIPTION_FORM_CLOSED, + }) + break + case InfiniteMessagesIds.appUpdateAvailable: + sendEventTelemetry({ + event: TelemetryEvent.UPDATE_NOTIFICATION_CLOSED, + }) + break + default: + break + } + + dispatch(removeInfiniteNotification(id)) + onCloseCallback?.() + }, + }, + { + variant: variant ?? riToast.Variant.Notice, + autoClose: ONE_HOUR, + }, + ) + // if this infinite toast id is already in the map, dismiss it + if (infiniteToastIdsRef.current.has(id)) { + riToast.dismiss(infiniteToastIdsRef.current.get(id)) + } + infiniteToastIdsRef.current.set(id, toastId) + }) + } + + useEffect(() => { + showInfiniteToasts(infiniteNotifications) + }, [infiniteNotifications]) +} diff --git a/redisinsight/ui/src/components/notifications/hooks/useMessageNotifications.tsx b/redisinsight/ui/src/components/notifications/hooks/useMessageNotifications.tsx new file mode 100644 index 0000000000..6ef1e53585 --- /dev/null +++ b/redisinsight/ui/src/components/notifications/hooks/useMessageNotifications.tsx @@ -0,0 +1,73 @@ +import React, { useEffect, useRef } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { riToast } from 'uiSrc/components/base/display/toast' +import { messagesSelector, removeMessage } from 'uiSrc/slices/app/notifications' +import { IMessage } from 'uiSrc/slices/interfaces' +import { setReleaseNotesViewed } from 'uiSrc/slices/app/info' +import { ColorText } from 'uiSrc/components/base/text' + +export const useMessageNotifications = () => { + const messagesData = useSelector(messagesSelector) + + const dispatch = useDispatch() + const toastIdsRef = useRef(new Map()) + const removeToast = (id: string) => { + if (toastIdsRef.current.has(id)) { + riToast.dismiss(toastIdsRef.current.get(id)) + toastIdsRef.current.delete(id) + } + dispatch(removeMessage(id)) + } + const onSubmitNotification = (id: string, group?: string) => { + if (group === 'upgrade') { + dispatch(setReleaseNotesViewed(true)) + } + dispatch(removeMessage(id)) + } + + const getSuccessText = (text: string | JSX.Element | JSX.Element[]) => ( + {text} + ) + const showSuccessToasts = (data: IMessage[]) => + data.forEach( + ({ + id = '', + title = '', + message = '', + showCloseButton = true, + actions, + className, + group, + }) => { + const handleClose = () => { + onSubmitNotification(id, group) + removeToast(id) + } + if (toastIdsRef.current.has(id)) { + removeToast(id) + return + } + const toastId = riToast( + { + className, + message: title, + description: getSuccessText(message), + actions: actions ?? { + primary: { + closes: true, + label: 'OK', + onClick: handleClose, + }, + }, + showCloseButton, + }, + { variant: riToast.Variant.Success, toastId: id }, + ) + toastIdsRef.current.set(id, toastId) + }, + ) + + useEffect(() => { + showSuccessToasts(messagesData) + }, [messagesData]) +} From 1a7d98f9a39a4fd4ae89fcd6b9978e8c57d0f5fd Mon Sep 17 00:00:00 2001 From: pd-redis Date: Thu, 16 Oct 2025 11:06:42 +0300 Subject: [PATCH 2/7] cleanup styles.module.scss --- .../DefaultErrorContent.tsx | 2 - .../notifications/styles.module.scss | 56 ------------------- .../notifications/success-messages.tsx | 37 ++++++++---- 3 files changed, 25 insertions(+), 70 deletions(-) delete mode 100644 redisinsight/ui/src/components/notifications/styles.module.scss diff --git a/redisinsight/ui/src/components/notifications/components/default-error-content/DefaultErrorContent.tsx b/redisinsight/ui/src/components/notifications/components/default-error-content/DefaultErrorContent.tsx index 76ea835662..2755119bc0 100644 --- a/redisinsight/ui/src/components/notifications/components/default-error-content/DefaultErrorContent.tsx +++ b/redisinsight/ui/src/components/notifications/components/default-error-content/DefaultErrorContent.tsx @@ -1,8 +1,6 @@ import React from 'react' import { ColorText } from 'uiSrc/components/base/text' -import { Spacer } from 'uiSrc/components/base/layout/spacer' -import { SecondaryButton } from 'uiSrc/components/base/forms/buttons' export interface Props { text: string | JSX.Element | JSX.Element[] diff --git a/redisinsight/ui/src/components/notifications/styles.module.scss b/redisinsight/ui/src/components/notifications/styles.module.scss deleted file mode 100644 index 817b08578e..0000000000 --- a/redisinsight/ui/src/components/notifications/styles.module.scss +++ /dev/null @@ -1,56 +0,0 @@ -.toastSuccessBtn { - background-color: var(--euiToastSuccessBtnColor) !important; - border: none !important; -} - -.list { - font: normal normal normal 12px/17px Graphik, sans-serif; - font-weight: 400; - padding-bottom: 10px; - - &:first-of-type { - padding-top: 10px; - } -} - -:global(.euiToast) { - box-shadow: none !important; -} - -.infiniteMessage { - :global { - .euiToastHeader { - display: none; - } - - .euiText, .euiTitle { - color: var(--euiColorPrimaryText) !important; - } - - .euiToast__closeButton { - opacity: 1; - } - - .infiniteMessage__title { - display: flex; - font-size: 18px; - line-height: 1.2; - margin-bottom: 8px; - - padding-right: 24px; - } - - .infiniteMessage__icon { - margin-right: 8px; - margin-top: 2px; - } - - .infiniteMessage__btn .euiButton__text { - color: var(--euiColorSecondaryText) !important; - } - } - - &:global(.euiToast.wide) { - width: 368px !important; - } -} diff --git a/redisinsight/ui/src/components/notifications/success-messages.tsx b/redisinsight/ui/src/components/notifications/success-messages.tsx index 6275acced6..f1f876ac90 100644 --- a/redisinsight/ui/src/components/notifications/success-messages.tsx +++ b/redisinsight/ui/src/components/notifications/success-messages.tsx @@ -1,4 +1,5 @@ import React from 'react' +import styled from 'styled-components' import { EXTERNAL_LINKS } from 'uiSrc/constants/links' import { IBulkActionOverview, @@ -14,10 +15,16 @@ import { import { numberWithSpaces } from 'uiSrc/utils/numbers' import { FlexItem, Row } from 'uiSrc/components/base/layout/flex' import { Text, Title } from 'uiSrc/components/base/text' -import styles from './styles.module.scss' -import { Spacer } from '../base/layout' +import { Spacer } from 'uiSrc/components/base/layout' + +const Li = styled.li>` + padding-bottom: 10px; + + &:first-of-type { + padding-top: 10px; + } +` -// TODO: use i18n file for texts export default { ADDED_NEW_INSTANCE: (instanceName: string) => ({ title: 'Database has been added', @@ -78,9 +85,11 @@ export default {
    {instanceNames.slice(0, limitShowRemovedInstances).map((el, i) => ( // eslint-disable-next-line react/no-array-index-key -
  • - {formatNameShort(el)} -
  • +
  • + + {formatNameShort(el)} + +
  • ))} {instanceNames.length >= limitShowRemovedInstances &&
  • ...
  • }
@@ -103,9 +112,11 @@ export default {
    {instanceNames.slice(0, limitShowRemovedInstances).map((el, i) => ( // eslint-disable-next-line react/no-array-index-key -
  • - {formatNameShort(el)} -
  • +
  • + + {formatNameShort(el)} + +
  • ))} {instanceNames.length >= limitShowRemovedInstances &&
  • ...
  • }
@@ -173,9 +184,11 @@ export default {
    {listOfElements.slice(0, limitShowRemovedElements).map((el, i) => ( // eslint-disable-next-line react/no-array-index-key -
  • - {formatNameShort(bufferToString(el))} -
  • +
  • + + {formatNameShort(bufferToString(el))} + +
  • ))} {listOfElements.length >= limitShowRemovedElements &&
  • ...
  • }
From 9d0324f9d61b3e655b39dad0be9751db4017614d Mon Sep 17 00:00:00 2001 From: pd-redis Date: Mon, 27 Oct 2025 09:44:43 +0200 Subject: [PATCH 3/7] refactor type of InfiniteMessages --- .../infinite-messages/InfiniteMessages.tsx | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteMessages.tsx b/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteMessages.tsx index 3cf468c670..38ce423fd6 100644 --- a/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteMessages.tsx +++ b/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteMessages.tsx @@ -38,11 +38,23 @@ const MANAGE_DB_LINK = getUtmExternalLink(EXTERNAL_LINKS.cloudConsole, { medium: UTM_MEDIUMS.Main, }) -// TODO: Refactor this type definition to work with the real parameters and their types we use in each message -export const INFINITE_MESSAGES: Record< - string, - (...args: any[]) => InfiniteMessage -> = { +interface InfiniteMessagesType { + AUTHENTICATING: () => InfiniteMessage + PENDING_CREATE_DB: (step?: CloudJobStep) => InfiniteMessage + SUCCESS_CREATE_DB: ( + details: Omit, + onSuccess: () => void, + jobName: Maybe, + ) => InfiniteMessage + DATABASE_EXISTS: (onSuccess?: () => void, onClose?: () => void) => InfiniteMessage + DATABASE_IMPORT_FORBIDDEN: (onClose?: () => void) => InfiniteMessage + SUBSCRIPTION_EXISTS: (onSuccess?: () => void, onClose?: () => void) => InfiniteMessage + AUTO_CREATING_DATABASE: () => InfiniteMessage + APP_UPDATE_AVAILABLE: (version: string, onSuccess?: () => void) => InfiniteMessage + SUCCESS_DEPLOY_PIPELINE: () => InfiniteMessage +} + +export const INFINITE_MESSAGES: InfiniteMessagesType = { AUTHENTICATING: () => ({ id: InfiniteMessagesIds.oAuthProgress, message: 'Authenticating…', From 178be4b217f0d2ad6a7c91015eae0136ad125957 Mon Sep 17 00:00:00 2001 From: pd-redis Date: Tue, 28 Oct 2025 15:13:07 +0200 Subject: [PATCH 4/7] refactor Notifications to only show single live notification at a time --- .gitignore | 4 + .../components/base/display/toast/RiToast.tsx | 13 ++- .../src/components/base/text/text.styles.ts | 1 + .../notifications/Notifications.tsx | 15 +-- .../infinite-messages/InfiniteMessages.tsx | 22 ++++- .../InfiniteNotifications.tsx | 93 +++++++++++++++++++ .../hooks/useInfiniteNotifications.ts | 55 ++++------- .../OAuthAutodiscovery.tsx | 1 + .../OAuthSocialButtons.tsx | 64 +++++++------ .../oauth-social-buttons/styles.module.scss | 3 - 10 files changed, 182 insertions(+), 89 deletions(-) create mode 100644 redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteNotifications.tsx diff --git a/.gitignore b/.gitignore index f25ae39dd6..c43a20fbd6 100644 --- a/.gitignore +++ b/.gitignore @@ -87,3 +87,7 @@ static/ .env* .npmrc + +# AI rules +.windsurfrules +.junie/ diff --git a/redisinsight/ui/src/components/base/display/toast/RiToast.tsx b/redisinsight/ui/src/components/base/display/toast/RiToast.tsx index 455ad6e2fb..c1dcb8db5f 100644 --- a/redisinsight/ui/src/components/base/display/toast/RiToast.tsx +++ b/redisinsight/ui/src/components/base/display/toast/RiToast.tsx @@ -9,6 +9,7 @@ import { ToastOptions as RcToastOptions } from 'react-toastify' import { CommonProps } from 'uiSrc/components/base/theme/types' import { ColorText, Text } from 'uiSrc/components/base/text' +import { ColorType } from 'uiSrc/components/base/text/text.styles' import { Spacer } from '../../layout' type RiToastProps = React.ComponentProps @@ -27,18 +28,15 @@ export const riToast = ( } if (typeof message === 'string') { - let color = options?.variant + let color: ColorType = options?.variant if (color === 'informative') { - // @ts-ignore color = 'subdued' } toastContent.message = ( - - - {message} - + + {message} - + ) } else { toastContent.message = message @@ -55,3 +53,4 @@ export const riToast = ( riToast.Variant = toast.Variant riToast.Position = toast.Position riToast.dismiss = toast.dismiss +riToast.isActive = toast.isActive diff --git a/redisinsight/ui/src/components/base/text/text.styles.ts b/redisinsight/ui/src/components/base/text/text.styles.ts index 6af3e4d879..c4454a754d 100644 --- a/redisinsight/ui/src/components/base/text/text.styles.ts +++ b/redisinsight/ui/src/components/base/text/text.styles.ts @@ -14,6 +14,7 @@ export type EuiColorNames = | 'accent' | 'warning' | 'success' + export type ColorType = BodyProps['color'] | EuiColorNames | (string & {}) export interface MapProps extends HTMLAttributes { $color?: ColorType diff --git a/redisinsight/ui/src/components/notifications/Notifications.tsx b/redisinsight/ui/src/components/notifications/Notifications.tsx index d9be9f4f2b..099fe00a21 100644 --- a/redisinsight/ui/src/components/notifications/Notifications.tsx +++ b/redisinsight/ui/src/components/notifications/Notifications.tsx @@ -1,17 +1,18 @@ import React from 'react' import { RiToaster } from 'uiSrc/components/base/display/toast' -import { - useErrorNotifications, - useMessageNotifications, - useInfiniteNotifications, -} from './hooks' +import { useErrorNotifications, useMessageNotifications } from './hooks' +import { InfiniteNotifications } from './components/infinite-messages/InfiniteNotifications' const Notifications = () => { useErrorNotifications() useMessageNotifications() - useInfiniteNotifications() - return + return ( + <> + + + + ) } export default Notifications diff --git a/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteMessages.tsx b/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteMessages.tsx index 38ce423fd6..fb86efa1cb 100644 --- a/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteMessages.tsx +++ b/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteMessages.tsx @@ -20,6 +20,7 @@ import { FlexItem, Row } from 'uiSrc/components/base/layout/flex' import { Spacer } from 'uiSrc/components/base/layout/spacer' import { PrimaryButton } from 'uiSrc/components/base/forms/buttons' import { RiIcon } from 'uiSrc/components/base/icons/RiIcon' + import styles from './styles.module.scss' export enum InfiniteMessagesIds { @@ -46,11 +47,20 @@ interface InfiniteMessagesType { onSuccess: () => void, jobName: Maybe, ) => InfiniteMessage - DATABASE_EXISTS: (onSuccess?: () => void, onClose?: () => void) => InfiniteMessage + DATABASE_EXISTS: ( + onSuccess?: () => void, + onClose?: () => void, + ) => InfiniteMessage DATABASE_IMPORT_FORBIDDEN: (onClose?: () => void) => InfiniteMessage - SUBSCRIPTION_EXISTS: (onSuccess?: () => void, onClose?: () => void) => InfiniteMessage + SUBSCRIPTION_EXISTS: ( + onSuccess?: () => void, + onClose?: () => void, + ) => InfiniteMessage AUTO_CREATING_DATABASE: () => InfiniteMessage - APP_UPDATE_AVAILABLE: (version: string, onSuccess?: () => void) => InfiniteMessage + APP_UPDATE_AVAILABLE: ( + version: string, + onSuccess?: () => void, + ) => InfiniteMessage SUCCESS_DEPLOY_PIPELINE: () => InfiniteMessage } @@ -219,8 +229,7 @@ export const INFINITE_MESSAGES: InfiniteMessagesType = { }), SUBSCRIPTION_EXISTS: (onSuccess?: () => void, onClose?: () => void) => ({ id: InfiniteMessagesIds.subscriptionExists, - message: - 'Your subscription does not have a free Redis Cloud database.', + message: 'Your subscription does not have a free Redis Cloud database.', description: 'Do you want to create a free database in your existing subscription?', actions: { @@ -268,3 +277,6 @@ export const INFINITE_MESSAGES: InfiniteMessagesType = { // } }), } + +export const IMContainerId = 'InfiniteMessages' +export const ONE_HOUR = 3_600_000 diff --git a/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteNotifications.tsx b/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteNotifications.tsx new file mode 100644 index 0000000000..cdd29d6c8e --- /dev/null +++ b/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteNotifications.tsx @@ -0,0 +1,93 @@ +import React, { useEffect, useRef } from 'react' + +import { useInfiniteNotifications } from 'uiSrc/components/notifications/hooks' +import { riToast, RiToaster } from 'uiSrc/components/base/display/toast' +import { IMContainerId, ONE_HOUR } from './InfiniteMessages' +import { InfiniteMessage } from 'uiSrc/slices/interfaces' + +const DISMISS_DELAY = 3000 // 3 seconds interval + +const showNotification = (notification: InfiniteMessage) => { + if (!notification) { + return + } + + // Show latest notification + return riToast(notification, { + containerId: IMContainerId, + }) +} + +export const InfiniteNotifications = () => { + const notifications = useInfiniteNotifications() + const queueRef = useRef[]>([]) + const dismissIntervalRef = useRef(null) + + const dismissFromQueue = () => { + if (queueRef.current.length > 0) { + const toastToRemove = queueRef.current.shift() // Remove from top of queue + if (toastToRemove) { + riToast.dismiss(toastToRemove) + } + } + } + + const startDismissInterval = () => { + if (dismissIntervalRef.current) { + clearInterval(dismissIntervalRef.current) + } + + dismissIntervalRef.current = window.setInterval(() => { + if (queueRef.current.length > 1) { + dismissFromQueue() + } else { + // Stop interval when queue has 1 or fewer items + if (dismissIntervalRef.current) { + clearInterval(dismissIntervalRef.current) + dismissIntervalRef.current = null + } + } + }, DISMISS_DELAY) + } + + const addToQueue = (toastId: ReturnType) => { + queueRef.current.push(toastId) // Add to end of queue + } + + const renderNotification = (notification: InfiniteMessage) => { + const toastId = showNotification(notification) + if (toastId) { + addToQueue(toastId) + } + } + + useEffect(() => { + if (notifications.length > 0) { + // Process each notification and add to queue + notifications.forEach((notification) => { + renderNotification(notification) + }) + } + // Start interval if queue has more than 1 item + if (queueRef.current.length > 1) { + startDismissInterval() + } + + // Cleanup on unmount + return () => { + if (dismissIntervalRef.current) { + clearInterval(dismissIntervalRef.current) + dismissIntervalRef.current = null + } + } + }, [notifications]) + + return ( + + ) +} diff --git a/redisinsight/ui/src/components/notifications/hooks/useInfiniteNotifications.ts b/redisinsight/ui/src/components/notifications/hooks/useInfiniteNotifications.ts index 59fc01757a..d2334b5780 100644 --- a/redisinsight/ui/src/components/notifications/hooks/useInfiniteNotifications.ts +++ b/redisinsight/ui/src/components/notifications/hooks/useInfiniteNotifications.ts @@ -1,27 +1,18 @@ -import { useEffect, useRef } from 'react' +import { useMemo } from 'react' import { useDispatch, useSelector } from 'react-redux' import { InfiniteMessage } from 'uiSrc/slices/interfaces' -import { riToast } from 'uiSrc/components/base/display/toast' -import { - infiniteNotificationsSelector, - removeInfiniteNotification, -} from 'uiSrc/slices/app/notifications' -import cx from 'classnames' +import { infiniteNotificationsSelector } from 'uiSrc/slices/app/notifications' import { InfiniteMessagesIds } from 'uiSrc/components/notifications/components' import { showOAuthProgress } from 'uiSrc/slices/oauth/cloud' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -const ONE_HOUR = 3_600_000 - export const useInfiniteNotifications = () => { const infiniteNotifications = useSelector(infiniteNotificationsSelector) const dispatch = useDispatch() - const infiniteToastIdsRef = useRef(new Map()) - - const showInfiniteToasts = (data: InfiniteMessage[]) => { - data.forEach((notification: InfiniteMessage) => { - const { + return useMemo(() => { + return infiniteNotifications.map( + ({ id, message, description, @@ -31,15 +22,16 @@ export const useInfiniteNotifications = () => { customIcon, showCloseButton = true, onClose: onCloseCallback, - } = notification - const toastId = riToast( - { - className: cx(className), - message: message, - description: description, + }: InfiniteMessage) => { + return { + id, + message, + description, actions, - showCloseButton, + className, + variant, customIcon, + showCloseButton, onClose: () => { switch (id) { case InfiniteMessagesIds.oAuthProgress: @@ -65,25 +57,10 @@ export const useInfiniteNotifications = () => { default: break } - - dispatch(removeInfiniteNotification(id)) onCloseCallback?.() }, - }, - { - variant: variant ?? riToast.Variant.Notice, - autoClose: ONE_HOUR, - }, - ) - // if this infinite toast id is already in the map, dismiss it - if (infiniteToastIdsRef.current.has(id)) { - riToast.dismiss(infiniteToastIdsRef.current.get(id)) - } - infiniteToastIdsRef.current.set(id, toastId) - }) - } - - useEffect(() => { - showInfiniteToasts(infiniteNotifications) + } + }, + ) }, [infiniteNotifications]) } diff --git a/redisinsight/ui/src/components/oauth/oauth-sso/oauth-autodiscovery/OAuthAutodiscovery.tsx b/redisinsight/ui/src/components/oauth/oauth-sso/oauth-autodiscovery/OAuthAutodiscovery.tsx index 637f9a1269..043b82506d 100644 --- a/redisinsight/ui/src/components/oauth/oauth-sso/oauth-autodiscovery/OAuthAutodiscovery.tsx +++ b/redisinsight/ui/src/components/oauth/oauth-sso/oauth-autodiscovery/OAuthAutodiscovery.tsx @@ -80,6 +80,7 @@ const OAuthAutodiscovery = (props: Props) => { {' '} account to auto-discover subscriptions and add your databases. + { const { onClick, className, inline, disabled } = props const agreement = useSelector(oauthCloudPAgreementSelector) - const socialLinks = [ - { - text: 'Google', - className: styles.googleButton, - icon: 'GoogleSigninIcon', - label: 'google-oauth', - strategy: OAuthStrategy.Google, - }, - { - text: 'Github', - className: styles.githubButton, - icon: 'GithubIcon', - label: 'github-oauth', - strategy: OAuthStrategy.GitHub, - }, - { - text: 'SSO', - className: styles.ssoButton, - icon: 'SsoIcon', - label: 'sso-oauth', - strategy: OAuthStrategy.SSO, - }, - ] - return ( -
@@ -61,6 +64,7 @@ const OAuthSocialButtons = (props: Props) => { data-testid={`${label}-tooltip`} > { data-testid={label} aria-labelledby={label} > - + {text} ))} -
+ ) } diff --git a/redisinsight/ui/src/components/oauth/shared/oauth-social-buttons/styles.module.scss b/redisinsight/ui/src/components/oauth/shared/oauth-social-buttons/styles.module.scss index bfef0a5ebc..e04b4bca56 100644 --- a/redisinsight/ui/src/components/oauth/shared/oauth-social-buttons/styles.module.scss +++ b/redisinsight/ui/src/components/oauth/shared/oauth-social-buttons/styles.module.scss @@ -1,7 +1,4 @@ .container { - display: flex; - align-items: center; - .button { height: auto !important; padding: 0 !important; From 8124c5c7d03d5f99663419384321d133e0f7c8b1 Mon Sep 17 00:00:00 2001 From: pd-redis Date: Tue, 28 Oct 2025 16:26:22 +0200 Subject: [PATCH 5/7] fix DbStatus.spec.tsx by setting up fake timers for tests --- .../home/components/db-status/DbStatus.spec.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/redisinsight/ui/src/pages/home/components/db-status/DbStatus.spec.tsx b/redisinsight/ui/src/pages/home/components/db-status/DbStatus.spec.tsx index 022736c619..6185921781 100644 --- a/redisinsight/ui/src/pages/home/components/db-status/DbStatus.spec.tsx +++ b/redisinsight/ui/src/pages/home/components/db-status/DbStatus.spec.tsx @@ -18,8 +18,22 @@ jest.mock('uiSrc/telemetry', () => ({ const mockedProps = mock() const daysToMs = (days: number) => days * 60 * 60 * 24 * 1000 +let mockDate: Date describe('DbStatus', () => { + beforeEach(() => { + jest.clearAllMocks() + + // Set up fake timers + jest.useFakeTimers() + mockDate = new Date('2024-11-22T12:00:00Z') + jest.setSystemTime(mockDate) + }) + + afterEach(() => { + jest.useRealTimers() + }) + it('should render', () => { expect(render()).toBeTruthy() }) From dba322b26b1e6b8817380116fd1445f453ce8811 Mon Sep 17 00:00:00 2001 From: pd-redis Date: Tue, 28 Oct 2025 16:44:10 +0200 Subject: [PATCH 6/7] fix IndexSection tests by adding error notifications to default toast container --- .../notifications/Notifications.tsx | 3 ++- .../infinite-messages/InfiniteMessages.tsx | 3 --- .../InfiniteNotifications.tsx | 2 +- .../src/components/notifications/constants.ts | 4 +++ .../notifications/error-messages.tsx | 25 ++++++++++++++++--- .../hooks/useMessageNotifications.tsx | 7 +++++- 6 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 redisinsight/ui/src/components/notifications/constants.ts diff --git a/redisinsight/ui/src/components/notifications/Notifications.tsx b/redisinsight/ui/src/components/notifications/Notifications.tsx index 099fe00a21..7181a8e95c 100644 --- a/redisinsight/ui/src/components/notifications/Notifications.tsx +++ b/redisinsight/ui/src/components/notifications/Notifications.tsx @@ -2,6 +2,7 @@ import React from 'react' import { RiToaster } from 'uiSrc/components/base/display/toast' import { useErrorNotifications, useMessageNotifications } from './hooks' import { InfiniteNotifications } from './components/infinite-messages/InfiniteNotifications' +import { defaultContainerId } from './constants' const Notifications = () => { useErrorNotifications() @@ -10,7 +11,7 @@ const Notifications = () => { return ( <> - + ) } diff --git a/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteMessages.tsx b/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteMessages.tsx index fb86efa1cb..1c76f9e30a 100644 --- a/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteMessages.tsx +++ b/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteMessages.tsx @@ -277,6 +277,3 @@ export const INFINITE_MESSAGES: InfiniteMessagesType = { // } }), } - -export const IMContainerId = 'InfiniteMessages' -export const ONE_HOUR = 3_600_000 diff --git a/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteNotifications.tsx b/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteNotifications.tsx index cdd29d6c8e..92bbc08e88 100644 --- a/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteNotifications.tsx +++ b/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteNotifications.tsx @@ -2,8 +2,8 @@ import React, { useEffect, useRef } from 'react' import { useInfiniteNotifications } from 'uiSrc/components/notifications/hooks' import { riToast, RiToaster } from 'uiSrc/components/base/display/toast' -import { IMContainerId, ONE_HOUR } from './InfiniteMessages' import { InfiniteMessage } from 'uiSrc/slices/interfaces' +import { IMContainerId, ONE_HOUR } from '../../constants' const DISMISS_DELAY = 3000 // 3 seconds interval diff --git a/redisinsight/ui/src/components/notifications/constants.ts b/redisinsight/ui/src/components/notifications/constants.ts new file mode 100644 index 0000000000..9d31e69b60 --- /dev/null +++ b/redisinsight/ui/src/components/notifications/constants.ts @@ -0,0 +1,4 @@ +export const defaultContainerId = 'default' + +export const IMContainerId = 'InfiniteMessages' +export const ONE_HOUR = 3_600_000 diff --git a/redisinsight/ui/src/components/notifications/error-messages.tsx b/redisinsight/ui/src/components/notifications/error-messages.tsx index a9d8a468eb..c8ae631e28 100644 --- a/redisinsight/ui/src/components/notifications/error-messages.tsx +++ b/redisinsight/ui/src/components/notifications/error-messages.tsx @@ -6,6 +6,7 @@ import { InfoIcon, ToastDangerIcon } from 'uiSrc/components/base/icons' import RdiDeployErrorContent from './components/rdi-deploy-error-content' import { EncryptionErrorContent, DefaultErrorContent } from './components' import CloudCapiUnAuthorizedErrorContent from './components/cloud-capi-unauthorized' +import { defaultContainerId } from './constants' // TODO: use i18n file for texts export default { @@ -29,7 +30,11 @@ export default { }, }, }, - { variant: riToast.Variant.Danger, toastId }, + { + variant: riToast.Variant.Danger, + toastId, + containerId: defaultContainerId, + }, ), ENCRYPTION: (onClose = () => {}, instanceId = '', toastId?: string) => riToast( @@ -42,7 +47,11 @@ export default { ), showCloseButton: false, }, - { variant: riToast.Variant.Danger, toastId }, + { + variant: riToast.Variant.Danger, + toastId, + containerId: defaultContainerId, + }, ), CLOUD_CAPI_KEY_UNAUTHORIZED: ( { @@ -70,7 +79,11 @@ export default { /> ), }, - { variant: riToast.Variant.Danger, toastId }, + { + variant: riToast.Variant.Danger, + toastId, + containerId: defaultContainerId, + }, ), RDI_DEPLOY_PIPELINE: ( { title, message }: { title?: string; message: string }, @@ -87,6 +100,10 @@ export default { ), }, - { variant: riToast.Variant.Danger, toastId }, + { + variant: riToast.Variant.Danger, + toastId, + containerId: defaultContainerId, + }, ), } diff --git a/redisinsight/ui/src/components/notifications/hooks/useMessageNotifications.tsx b/redisinsight/ui/src/components/notifications/hooks/useMessageNotifications.tsx index 6ef1e53585..27c2876e31 100644 --- a/redisinsight/ui/src/components/notifications/hooks/useMessageNotifications.tsx +++ b/redisinsight/ui/src/components/notifications/hooks/useMessageNotifications.tsx @@ -5,6 +5,7 @@ import { messagesSelector, removeMessage } from 'uiSrc/slices/app/notifications' import { IMessage } from 'uiSrc/slices/interfaces' import { setReleaseNotesViewed } from 'uiSrc/slices/app/info' import { ColorText } from 'uiSrc/components/base/text' +import { defaultContainerId } from '../constants' export const useMessageNotifications = () => { const messagesData = useSelector(messagesSelector) @@ -61,7 +62,11 @@ export const useMessageNotifications = () => { }, showCloseButton, }, - { variant: riToast.Variant.Success, toastId: id }, + { + variant: riToast.Variant.Success, + toastId: id, + containerId: defaultContainerId, + }, ) toastIdsRef.current.set(id, toastId) }, From 4c64b06f033deca4d5b2b29c0d1e1ff79f83a703 Mon Sep 17 00:00:00 2001 From: pd-redis Date: Wed, 29 Oct 2025 10:27:33 +0200 Subject: [PATCH 7/7] fix refactor error-messages generation --- .../infinite-messages/InfiniteMessages.tsx | 1 + .../notifications/error-messages.tsx | 122 ++++++------------ .../hooks/useErrorNotifications.ts | 23 ++-- .../ui/src/slices/app/notifications.ts | 8 ++ redisinsight/ui/src/slices/interfaces/app.ts | 1 + 5 files changed, 62 insertions(+), 93 deletions(-) diff --git a/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteMessages.tsx b/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteMessages.tsx index 1c76f9e30a..f64d5dd810 100644 --- a/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteMessages.tsx +++ b/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteMessages.tsx @@ -74,6 +74,7 @@ export const INFINITE_MESSAGES: InfiniteMessagesType = { PENDING_CREATE_DB: (step?: CloudJobStep) => ({ id: InfiniteMessagesIds.oAuthProgress, customIcon: LoaderLargeIcon, + variation: step, message: ( <> {(step === CloudJobStep.Credentials || !step) && diff --git a/redisinsight/ui/src/components/notifications/error-messages.tsx b/redisinsight/ui/src/components/notifications/error-messages.tsx index c8ae631e28..09133d8afd 100644 --- a/redisinsight/ui/src/components/notifications/error-messages.tsx +++ b/redisinsight/ui/src/components/notifications/error-messages.tsx @@ -1,58 +1,34 @@ import React from 'react' -import { riToast } from 'uiSrc/components/base/display/toast' import { InfoIcon, ToastDangerIcon } from 'uiSrc/components/base/icons' import RdiDeployErrorContent from './components/rdi-deploy-error-content' import { EncryptionErrorContent, DefaultErrorContent } from './components' import CloudCapiUnAuthorizedErrorContent from './components/cloud-capi-unauthorized' -import { defaultContainerId } from './constants' -// TODO: use i18n file for texts export default { - DEFAULT: ( - text: any, - onClose = () => {}, - title: string = 'Error', - toastId?: string, - ) => - riToast( - { - 'data-testid': 'toast-error', - customIcon: ToastDangerIcon, - message: title, - description: , - actions: { - primary: { - label: 'OK', - closes: true, - onClick: onClose, - }, - }, - }, - { - variant: riToast.Variant.Danger, - toastId, - containerId: defaultContainerId, - }, - ), - ENCRYPTION: (onClose = () => {}, instanceId = '', toastId?: string) => - riToast( - { - 'data-testid': 'toast-error-encryption', - customIcon: InfoIcon, - message: 'Unable to decrypt', - description: ( - - ), - showCloseButton: false, - }, - { - variant: riToast.Variant.Danger, - toastId, - containerId: defaultContainerId, + DEFAULT: (text: any, onClose = () => {}, title: string = 'Error') => ({ + 'data-testid': 'toast-error', + customIcon: ToastDangerIcon, + message: title, + description: , + actions: { + primary: { + label: 'OK', + closes: true, + onClick: onClose, }, + }, + }), + ENCRYPTION: (onClose = () => {}, instanceId = '') => ({ + 'data-testid': 'toast-error-encryption', + customIcon: InfoIcon, + message: 'Unable to decrypt', + description: ( + ), + showCloseButton: false, + }), CLOUD_CAPI_KEY_UNAUTHORIZED: ( { message, @@ -63,47 +39,27 @@ export default { }, additionalInfo: Record, onClose: () => void, - toastId?: string, - ) => - riToast( - { - 'data-testid': 'toast-error-cloud-capi-key-unauthorized', - customIcon: ToastDangerIcon, - message: title, - showCloseButton: false, - description: ( - - ), - }, - { - variant: riToast.Variant.Danger, - toastId, - containerId: defaultContainerId, - }, + ) => ({ + 'data-testid': 'toast-error-cloud-capi-key-unauthorized', + customIcon: ToastDangerIcon, + message: title, + showCloseButton: false, + description: ( + ), + }), RDI_DEPLOY_PIPELINE: ( { title, message }: { title?: string; message: string }, onClose: () => void, - toastId?: string, - ) => - riToast( - { - 'data-testid': 'toast-error-deploy', - customIcon: ToastDangerIcon, - onClose, - message: title, - description: ( - - ), - }, - { - variant: riToast.Variant.Danger, - toastId, - containerId: defaultContainerId, - }, - ), + ) => ({ + 'data-testid': 'toast-error-deploy', + customIcon: ToastDangerIcon, + onClose, + message: title, + description: , + }), } diff --git a/redisinsight/ui/src/components/notifications/hooks/useErrorNotifications.ts b/redisinsight/ui/src/components/notifications/hooks/useErrorNotifications.ts index 7a837ff4d8..9e5bef21db 100644 --- a/redisinsight/ui/src/components/notifications/hooks/useErrorNotifications.ts +++ b/redisinsight/ui/src/components/notifications/hooks/useErrorNotifications.ts @@ -8,6 +8,8 @@ import { ApiEncryptionErrors } from 'uiSrc/constants/apiErrors' import errorMessages from 'uiSrc/components/notifications/error-messages' import { CustomErrorCodes } from 'uiSrc/constants' import { errorsSelector, removeMessage } from 'uiSrc/slices/app/notifications' +import { defaultContainerId } from 'uiSrc/components/notifications/constants' +import { RiToastType } from 'uiSrc/components/base/display/toast/RiToast' const DEFAULT_ERROR_TITLE = 'Error' @@ -36,41 +38,42 @@ export const useErrorNotifications = () => { removeToast(id) return } - let toastId: ReturnType + + let errorMessage: RiToastType if (ApiEncryptionErrors.includes(name)) { - toastId = errorMessages.ENCRYPTION( + errorMessage = errorMessages.ENCRYPTION( () => removeToast(id), instanceId, - id, ) } else if ( additionalInfo?.errorCode === CustomErrorCodes.CloudCapiKeyUnauthorized ) { - toastId = errorMessages.CLOUD_CAPI_KEY_UNAUTHORIZED( + errorMessage = errorMessages.CLOUD_CAPI_KEY_UNAUTHORIZED( { message, title }, additionalInfo, () => removeToast(id), - id, ) } else if ( additionalInfo?.errorCode === CustomErrorCodes.RdiDeployPipelineFailure ) { - toastId = errorMessages.RDI_DEPLOY_PIPELINE( + errorMessage = errorMessages.RDI_DEPLOY_PIPELINE( { title, message }, () => removeToast(id), - id, ) } else { - toastId = errorMessages.DEFAULT( + errorMessage = errorMessages.DEFAULT( message, () => removeToast(id), title, - id, ) } - + const toastId: ReturnType = riToast(errorMessage, { + variant: riToast.Variant.Danger, + toastId: id, + containerId: defaultContainerId, + }) toastIdsRef.current.set(id, toastId) }, ) diff --git a/redisinsight/ui/src/slices/app/notifications.ts b/redisinsight/ui/src/slices/app/notifications.ts index d901ac827b..e7d37457ad 100644 --- a/redisinsight/ui/src/slices/app/notifications.ts +++ b/redisinsight/ui/src/slices/app/notifications.ts @@ -168,6 +168,14 @@ const notificationsSlice = createSlice({ if (index === -1) { state.infiniteMessages.push(payload) } else { + const currentNotification = state.infiniteMessages[index] + // check if existing notification is exactly the same as the new one, if yes, do not update + if ( + currentNotification.variation && + payload.variation === currentNotification.variation + ) { + return + } state.infiniteMessages[index] = payload } }, diff --git a/redisinsight/ui/src/slices/interfaces/app.ts b/redisinsight/ui/src/slices/interfaces/app.ts index 525567331e..26d5240ea2 100644 --- a/redisinsight/ui/src/slices/interfaces/app.ts +++ b/redisinsight/ui/src/slices/interfaces/app.ts @@ -247,6 +247,7 @@ export interface IGlobalNotification { export interface InfiniteMessage { id: string + variation?: string variant?: ToastVariant className?: string message?: RiToastType['message']