Skip to content
Open
Show file tree
Hide file tree
Changes from all 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