diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html
index a3407dc6d5..8157444099 100644
--- a/.storybook/preview-head.html
+++ b/.storybook/preview-head.html
@@ -1,6 +1,20 @@
+
+
+
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/ErrorNotifications.stories.tsx b/redisinsight/ui/src/components/notifications/ErrorNotifications.stories.tsx
new file mode 100644
index 0000000000..21bc0fea14
--- /dev/null
+++ b/redisinsight/ui/src/components/notifications/ErrorNotifications.stories.tsx
@@ -0,0 +1,86 @@
+import React from 'react'
+import { faker } from '@faker-js/faker'
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { action } from 'storybook/actions'
+
+import { PrimaryButton } from 'uiSrc/components/base/forms/buttons'
+import { Row } from 'uiSrc/components/base/layout/flex'
+
+import Notifications from './Notifications'
+import ERROR_MESSAGES from './error-messages'
+
+const meta = {
+ component: Notifications,
+} satisfies Meta
+
+export default meta
+
+type Story = StoryObj
+
+const ErrorNotifications = () => {
+ return (
+ <>
+
+
+ {
+ ERROR_MESSAGES.DEFAULT(
+ faker.lorem.sentence(),
+ action('default error close'),
+ 'Error',
+ )
+ }}
+ size="small"
+ >
+ Default Error
+
+ {
+ ERROR_MESSAGES.ENCRYPTION(
+ action('encryption error close'),
+ faker.database.mongodbObjectId(),
+ )
+ }}
+ size="small"
+ >
+ Encryption Error
+
+ {
+ ERROR_MESSAGES.CLOUD_CAPI_KEY_UNAUTHORIZED(
+ {
+ message: 'Your API key is unauthorized to access this resource',
+ title: 'Unauthorized',
+ },
+ {
+ resourceId: faker.string.uuid(),
+ },
+ action('cloud capi unauthorized close'),
+ )
+ }}
+ size="small"
+ >
+ Cloud CAPI Unauthorized
+
+ {
+ ERROR_MESSAGES.RDI_DEPLOY_PIPELINE(
+ {
+ title: 'Pipeline deployment failed',
+ message: faker.lorem.paragraph(),
+ },
+ action('rdi deploy error close'),
+ )
+ }}
+ size="small"
+ >
+ RDI Deploy Pipeline Error
+
+
+ >
+ )
+}
+
+export const Errors: Story = {
+ render: () => ,
+}
diff --git a/redisinsight/ui/src/components/notifications/InfiniteNotifications.stories.tsx b/redisinsight/ui/src/components/notifications/InfiniteNotifications.stories.tsx
new file mode 100644
index 0000000000..733c267724
--- /dev/null
+++ b/redisinsight/ui/src/components/notifications/InfiniteNotifications.stories.tsx
@@ -0,0 +1,216 @@
+import React from 'react'
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { useDispatch } from 'react-redux'
+import { action } from 'storybook/actions'
+
+import { addInfiniteNotification } from 'uiSrc/slices/app/notifications'
+import { PrimaryButton } from 'uiSrc/components/base/forms/buttons'
+import { Row } from 'uiSrc/components/base/layout/flex'
+import { INFINITE_MESSAGES } from 'uiSrc/components/notifications/components'
+import { CloudJobName, CloudJobStep } from 'uiSrc/electron/constants'
+import { OAuthProvider } from 'uiSrc/components/oauth/oauth-select-plan/constants'
+
+import Notifications from './Notifications'
+
+const meta = {
+ component: Notifications,
+} satisfies Meta
+
+export default meta
+
+type Story = StoryObj
+
+const InfiniteNotifications = () => {
+ const dispatch = useDispatch()
+
+ return (
+ <>
+
+
+ {
+ dispatch(
+ addInfiniteNotification(INFINITE_MESSAGES.AUTHENTICATING()),
+ )
+ }}
+ size="small"
+ >
+ Authenticating
+
+ {
+ dispatch(
+ addInfiniteNotification(
+ INFINITE_MESSAGES.PENDING_CREATE_DB(CloudJobStep.Credentials),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Pending Create DB (Credentials)
+
+ {
+ dispatch(
+ addInfiniteNotification(
+ INFINITE_MESSAGES.PENDING_CREATE_DB(CloudJobStep.Subscription),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Pending Create DB (Subscription)
+
+ {
+ dispatch(
+ addInfiniteNotification(
+ INFINITE_MESSAGES.PENDING_CREATE_DB(CloudJobStep.Database),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Pending Create DB (Database)
+
+ {
+ dispatch(
+ addInfiniteNotification(
+ INFINITE_MESSAGES.PENDING_CREATE_DB(CloudJobStep.Import),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Pending Create DB (Import)
+
+ {
+ dispatch(
+ addInfiniteNotification(
+ INFINITE_MESSAGES.SUCCESS_CREATE_DB(
+ {
+ provider: OAuthProvider.AWS,
+ region: 'us-east-1',
+ },
+ action('create aws success'),
+ CloudJobName.CreateFreeDatabase,
+ ),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Success Create DB (AWS)
+
+ {
+ dispatch(
+ addInfiniteNotification(
+ INFINITE_MESSAGES.SUCCESS_CREATE_DB(
+ {
+ provider: OAuthProvider.Google,
+ region: 'us-central1',
+ },
+ action('create gcp success'),
+ CloudJobName.CreateFreeSubscriptionAndDatabase,
+ ),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Success Create DB (GCP)
+
+ {
+ dispatch(
+ addInfiniteNotification(
+ INFINITE_MESSAGES.DATABASE_EXISTS(
+ action('db exists success'),
+ action('db exists close'),
+ ),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Database Exists
+
+ {
+ dispatch(
+ addInfiniteNotification(
+ INFINITE_MESSAGES.DATABASE_IMPORT_FORBIDDEN(
+ action('db import forbidden close'),
+ ),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Database Import Forbidden
+
+ {
+ dispatch(
+ addInfiniteNotification(
+ INFINITE_MESSAGES.SUBSCRIPTION_EXISTS(
+ action('subscription exists success'),
+ action('subscription exists close'),
+ ),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Subscription Exists
+
+ {
+ dispatch(
+ addInfiniteNotification(
+ INFINITE_MESSAGES.AUTO_CREATING_DATABASE(),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Auto Creating Database
+
+ {
+ dispatch(
+ addInfiniteNotification(
+ INFINITE_MESSAGES.APP_UPDATE_AVAILABLE(
+ '2.60.0',
+ action('app update available success'),
+ ),
+ ),
+ )
+ }}
+ size="small"
+ >
+ App Update Available
+
+ {
+ dispatch(
+ addInfiniteNotification(
+ INFINITE_MESSAGES.SUCCESS_DEPLOY_PIPELINE(),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Success Deploy Pipeline
+
+
+ >
+ )
+}
+
+export const Infinite: Story = {
+ render: () => ,
+}
diff --git a/redisinsight/ui/src/components/notifications/MessageNotifications.stories.tsx b/redisinsight/ui/src/components/notifications/MessageNotifications.stories.tsx
new file mode 100644
index 0000000000..d5a2ca0451
--- /dev/null
+++ b/redisinsight/ui/src/components/notifications/MessageNotifications.stories.tsx
@@ -0,0 +1,377 @@
+import React, { useEffect } from 'react'
+import { faker } from '@faker-js/faker'
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { action } from 'storybook/actions'
+import { useDispatch } from 'react-redux'
+
+import {
+ addMessageNotification,
+ resetMessages,
+} from 'uiSrc/slices/app/notifications'
+import { PrimaryButton } from 'uiSrc/components/base/forms/buttons'
+import { Row } from 'uiSrc/components/base/layout/flex'
+import { stringToBuffer } from 'uiSrc/utils'
+import { BulkActionsStatus, RedisDataType } from 'uiSrc/constants'
+
+import Notifications from './Notifications'
+import SUCCESS_MESSAGES from './success-messages'
+import { BulkActionType } from 'apiSrc/modules/bulk-actions/constants'
+
+const meta = {
+ component: Notifications,
+} satisfies Meta
+
+export default meta
+
+type Story = StoryObj
+
+const MessageNotifications = () => {
+ const dispatch = useDispatch()
+
+ useEffect(() => {
+ dispatch(resetMessages())
+ }, [])
+
+ return (
+ <>
+
+
+ {
+ dispatch(
+ addMessageNotification(
+ SUCCESS_MESSAGES.ADDED_NEW_INSTANCE(
+ faker.database.mongodbObjectId(),
+ ),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Added New Instance
+
+ {
+ dispatch(
+ addMessageNotification(
+ SUCCESS_MESSAGES.ADDED_NEW_RDI_INSTANCE(
+ faker.database.mongodbObjectId(),
+ ),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Added New RDI Instance
+
+ {
+ dispatch(
+ addMessageNotification(
+ SUCCESS_MESSAGES.DELETE_INSTANCE(
+ faker.database.mongodbObjectId(),
+ ),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Delete Instance
+
+ {
+ dispatch(
+ addMessageNotification(
+ SUCCESS_MESSAGES.DELETE_RDI_INSTANCE(
+ faker.database.mongodbObjectId(),
+ ),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Delete RDI Instance
+
+ {
+ const instanceNames = Array.from({ length: 12 }, () =>
+ faker.database.mongodbObjectId(),
+ )
+ dispatch(
+ addMessageNotification(
+ SUCCESS_MESSAGES.DELETE_INSTANCES(instanceNames),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Delete Multiple Instances
+
+ {
+ const instanceNames = Array.from({ length: 12 }, () =>
+ faker.database.mongodbObjectId(),
+ )
+ dispatch(
+ addMessageNotification(
+ SUCCESS_MESSAGES.DELETE_RDI_INSTANCES(instanceNames),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Delete Multiple RDI Instances
+
+ {
+ dispatch(
+ addMessageNotification(
+ SUCCESS_MESSAGES.ADDED_NEW_KEY(
+ stringToBuffer(faker.lorem.word()),
+ ),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Added New Key
+
+ {
+ dispatch(
+ addMessageNotification(
+ SUCCESS_MESSAGES.DELETED_KEY(
+ stringToBuffer(faker.lorem.word()),
+ ),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Deleted Key
+
+ {
+ dispatch(
+ addMessageNotification(
+ SUCCESS_MESSAGES.REMOVED_KEY_VALUE(
+ stringToBuffer(faker.lorem.word()),
+ stringToBuffer(faker.lorem.word()),
+ 'Member',
+ ),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Removed Key Value
+
+ {
+ const elements = Array.from({ length: 12 }, () =>
+ stringToBuffer(faker.lorem.word()),
+ )
+ dispatch(
+ addMessageNotification(
+ SUCCESS_MESSAGES.REMOVED_LIST_ELEMENTS(
+ stringToBuffer(faker.lorem.word()),
+ elements.length,
+ elements,
+ ),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Removed List Elements
+
+ {
+ dispatch(
+ addMessageNotification(
+ SUCCESS_MESSAGES.INSTALLED_NEW_UPDATE(
+ '2.70.0',
+ action('update link click'),
+ ),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Installed New Update
+
+ {
+ dispatch(
+ addMessageNotification(
+ SUCCESS_MESSAGES.MESSAGE_ACTION(
+ faker.lorem.sentence(),
+ 'claimed',
+ ),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Message Action
+
+ {
+ dispatch(
+ addMessageNotification(SUCCESS_MESSAGES.NO_CLAIMED_MESSAGES()),
+ )
+ }}
+ size="small"
+ >
+ No Claimed Messages
+
+ {
+ dispatch(addMessageNotification(SUCCESS_MESSAGES.CREATE_INDEX()))
+ }}
+ size="small"
+ >
+ Create Index
+
+ {
+ dispatch(
+ addMessageNotification(
+ SUCCESS_MESSAGES.DELETE_INDEX(faker.lorem.word()),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Delete Index
+
+ {
+ dispatch(addMessageNotification(SUCCESS_MESSAGES.TEST_CONNECTION()))
+ }}
+ size="small"
+ >
+ Test Connection
+
+ {
+ dispatch(
+ addMessageNotification(
+ SUCCESS_MESSAGES.UPLOAD_DATA_BULK(
+ {
+ id: faker.string.uuid(),
+ databaseId: faker.string.uuid(),
+ type: BulkActionType.Upload,
+ status: BulkActionsStatus.Completed,
+ filter: {
+ type: RedisDataType.String,
+ match: '*',
+ },
+ progress: {
+ total: 1000,
+ scanned: 1000,
+ },
+ summary: {
+ processed: 1000,
+ succeed: 950,
+ failed: 50,
+ errors: [],
+ keys: [],
+ },
+ duration: 12345,
+ },
+ 'bulk-data.txt',
+ ),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Upload Data Bulk
+
+ {
+ dispatch(
+ addMessageNotification(
+ SUCCESS_MESSAGES.DELETE_LIBRARY(faker.lorem.word()),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Delete Library
+
+ {
+ dispatch(
+ addMessageNotification(
+ SUCCESS_MESSAGES.ADD_LIBRARY(faker.lorem.word()),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Add Library
+
+ {
+ dispatch(
+ addMessageNotification(SUCCESS_MESSAGES.REMOVED_ALL_CAPI_KEYS()),
+ )
+ }}
+ size="small"
+ >
+ Removed All CAPI Keys
+
+ {
+ dispatch(
+ addMessageNotification(
+ SUCCESS_MESSAGES.REMOVED_CAPI_KEY(faker.lorem.word()),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Removed CAPI Key
+
+ {
+ dispatch(
+ addMessageNotification(
+ SUCCESS_MESSAGES.DATABASE_ALREADY_EXISTS(),
+ ),
+ )
+ }}
+ size="small"
+ >
+ Database Already Exists
+
+ {
+ dispatch(
+ addMessageNotification(SUCCESS_MESSAGES.SUCCESS_RESET_PIPELINE()),
+ )
+ }}
+ size="small"
+ >
+ Success Reset Pipeline
+
+ {
+ dispatch(
+ addMessageNotification(SUCCESS_MESSAGES.SUCCESS_TAGS_UPDATED()),
+ )
+ }}
+ size="small"
+ >
+ Success Tags Updated
+
+
+ >
+ )
+}
+
+export const Messages: Story = {
+ render: () => ,
+}
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/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/components/infinite-messages/InfiniteMessages.tsx b/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteMessages.tsx
index 655dfa2f1b..b9677b0c8d 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…',
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])
+}
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 && - ...
}