Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,7 @@ static/

.env*
.npmrc

# AI rules
.windsurfrules
.junie/
13 changes: 6 additions & 7 deletions redisinsight/ui/src/components/base/display/toast/RiToast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof Toast>
Expand All @@ -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 = (
<ColorText color={color}>
<Text size="M" variant="semiBold">
{message}
</Text>
<Text size="M" variant="semiBold">
<ColorText color={color}>{message}</ColorText>
<Spacer size="s" />
</ColorText>
</Text>
)
} else {
toastContent.message = message
Expand All @@ -55,3 +53,4 @@ export const riToast = (
riToast.Variant = toast.Variant
riToast.Position = toast.Position
riToast.dismiss = toast.dismiss
riToast.isActive = toast.isActive
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@
} = props

const ArrowIcon = () => (
<RiIcon type="ArrowDiagonalIcon" size={iconSize} color="informative400" />
<RiIcon
type="ArrowDiagonalIcon"
size={iconSize || size}

Check warning on line 27 in redisinsight/ui/src/components/base/external-link/ExternalLink.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
color="informative400"
/>
)

return (
Expand Down
1 change: 1 addition & 0 deletions redisinsight/ui/src/components/base/text/text.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type EuiColorNames =
| 'accent'
| 'warning'
| 'success'

export type ColorType = BodyProps['color'] | EuiColorNames | (string & {})
export interface MapProps extends HTMLAttributes<HTMLElement> {
$color?: ColorType
Expand Down
237 changes: 13 additions & 224 deletions redisinsight/ui/src/components/notifications/Notifications.tsx
Original file line number Diff line number Diff line change
@@ -1,230 +1,19 @@
import React, { useEffect, useRef } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import cx from 'classnames'
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'
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 = () => {
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[]) => (
<ColorText color="success">{text}</ColorText>
useErrorNotifications()
useMessageNotifications()

return (
<>
<InfiniteNotifications />
<RiToaster containerId={defaultContainerId} />
</>
)

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<typeof riToast>
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<number | string>())

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])

return <RiToaster />
}

export default Notifications
Original file line number Diff line number Diff line change
@@ -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[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -38,11 +39,32 @@ 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<CloudSuccessResult, 'resourceId'>,
onSuccess: () => void,
jobName: Maybe<CloudJobName>,
) => 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…',
Expand All @@ -52,6 +74,7 @@ export const INFINITE_MESSAGES: Record<
PENDING_CREATE_DB: (step?: CloudJobStep) => ({
id: InfiniteMessagesIds.oAuthProgress,
customIcon: LoaderLargeIcon,
variation: step,
message: (
<>
{(step === CloudJobStep.Credentials || !step) &&
Expand Down Expand Up @@ -207,8 +230,7 @@ export const INFINITE_MESSAGES: Record<
}),
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: {
Expand Down
Loading
Loading