Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
OAuthSelectAccountDialog,
OAuthSelectPlan,
OAuthSignInDialog,
OAuthSocialDialog,
} from 'uiSrc/components'
import { FeatureFlags } from 'uiSrc/constants'

Expand All @@ -13,6 +14,7 @@ const GlobalDialogs = () => (
<OAuthSignInDialog />
<OAuthSelectAccountDialog />
<OAuthSelectPlan />
<OAuthSocialDialog />
</FeatureFlagComponent>
</>
)
Expand Down
11 changes: 10 additions & 1 deletion redisinsight/ui/src/components/notifications/Notifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ 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 errorMessages from './error-messages'
import { InfiniteMessagesIds } from './components'

Expand Down Expand Up @@ -90,10 +91,18 @@ const Notifications = () => {
})

const getErrorsToasts = (errors: IError[]) =>
errors.map(({ id = '', message = DEFAULT_ERROR_MESSAGE, instanceId = '', name, title }) => {
errors.map(({ id = '', message = DEFAULT_ERROR_MESSAGE, instanceId = '', name, title, additionalInfo }) => {
if (ApiEncryptionErrors.includes(name)) {
return errorMessages.ENCRYPTION(id, () => removeToast({ id }), instanceId)
}

if (additionalInfo?.errorCode === CustomErrorCodes.CloudCapiKeyUnauthorized) {
return errorMessages.CLOUD_CAPI_KEY_UNAUTHORIZED(
{ id, message, title },
additionalInfo,
() => removeToast({ id })
)
}
return errorMessages.DEFAULT(id, message, () => removeToast({ id }), title)
})

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React from 'react'
import { mock } from 'ts-mockito'
import { cloneDeep } from 'lodash'
import reactRouterDom from 'react-router-dom'
import { render, screen, fireEvent, mockedStore, cleanup, act } from 'uiSrc/utils/test-utils'

import { removeCapiKey } from 'uiSrc/slices/oauth/cloud'
import { apiService } from 'uiSrc/services'
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
import { OAuthSocialSource } from 'uiSrc/slices/interfaces'

import CloudCapiUnAuthorizedErrorContent, { Props } from './CloudCapiUnAuthorizedErrorContent'

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: () => ({
push: jest.fn,
}),
}))

jest.mock('uiSrc/telemetry', () => ({
...jest.requireActual('uiSrc/telemetry'),
sendEventTelemetry: jest.fn(),
}))

let store: typeof mockedStore
beforeEach(() => {
cleanup()
store = cloneDeep(mockedStore)
store.clearActions()
})

const mockedProps = mock<Props>()
describe('CloudCapiUnAuthorizedErrorContent', () => {
it('should render', () => {
expect(render(<CloudCapiUnAuthorizedErrorContent {...mockedProps} />)).toBeTruthy()
})

it('should сall proper action on delete', () => {
const onClose = jest.fn()
render(<CloudCapiUnAuthorizedErrorContent {...mockedProps} onClose={onClose} />)

fireEvent.click(screen.getByTestId('remove-api-key-btn'))

expect(store.getActions()).toEqual([removeCapiKey()])
expect(onClose).toBeCalled()
})

it('should сall proper history push on go to settings', () => {
const pushMock = jest.fn()
const onClose = jest.fn()
reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })
render(<CloudCapiUnAuthorizedErrorContent {...mockedProps} onClose={onClose} />)

fireEvent.click(screen.getByTestId('go-to-settings-btn'))

expect(pushMock).toBeCalledWith('/settings#cloud')
expect(onClose).toBeCalled()
})

it('should сall proper telemetry on delete', async () => {
const sendEventTelemetryMock = jest.fn();
(sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock)
apiService.delete = jest.fn().mockResolvedValueOnce({ status: 200 })

render(<CloudCapiUnAuthorizedErrorContent {...mockedProps} resourceId="123" />)

await act(() => {
fireEvent.click(screen.getByTestId('remove-api-key-btn'))
})

expect(sendEventTelemetry).toBeCalledWith({
event: TelemetryEvent.CLOUD_API_KEY_REMOVED,
eventData: {
source: OAuthSocialSource.ConfirmationMessage
}
});

(sendEventTelemetry as jest.Mock).mockRestore()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTextColor } from '@elastic/eui'
import React from 'react'
import { useDispatch } from 'react-redux'
import { useHistory } from 'react-router-dom'
import { removeCapiKeyAction } from 'uiSrc/slices/oauth/cloud'
import { Pages } from 'uiSrc/constants'
import { OAuthSocialSource } from 'uiSrc/slices/interfaces'
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'

export interface Props {
resourceId: string
text: string | JSX.Element | JSX.Element[]
onClose?: () => void
}

const CloudCapiUnAuthorizedErrorContent = (
{
text,
onClose = () => {},
resourceId,
}: Props
) => {
const dispatch = useDispatch()
const history = useHistory()

const handleRemoveCapi = () => {
dispatch(removeCapiKeyAction({ id: resourceId, name: 'Api Key' }, () => {
sendEventTelemetry({
event: TelemetryEvent.CLOUD_API_KEY_REMOVED,
eventData: {
source: OAuthSocialSource.ConfirmationMessage
}
})
}))
onClose?.()
}

const handleGoToSettings = () => {
history.push(`${Pages.settings}#cloud`)
onClose?.()
}

return (
<>
<EuiTextColor color="ghost">{text}</EuiTextColor>
<EuiSpacer />
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton
size="s"
color="warning"
onClick={handleGoToSettings}
className="toast-danger-btn euiBorderWidthThick"
data-testid="go-to-settings-btn"
>
Go to Settings
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
size="s"
color="warning"
onClick={handleRemoveCapi}
className="toast-danger-btn"
data-testid="remove-api-key-btn"
>
Remove API key
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>
)
}

export default CloudCapiUnAuthorizedErrorContent
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import CloudCapiUnAuthorizedErrorContent from './CloudCapiUnAuthorizedErrorContent'

export default CloudCapiUnAuthorizedErrorContent
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import DefaultErrorContent from './DefaultErrorContent'

export default DefaultErrorContent
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import EncryptionErrorContent from './EncryptionErrorContent'

export default EncryptionErrorContent
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import DefaultErrorContent from './DefaultErrorContent'
import EncryptionErrorContent from './EncryptionErrorContent'
import { INFINITE_MESSAGES, InfiniteMessagesIds } from './InfiniteMessages'
import DefaultErrorContent from './default-error-content'
import EncryptionErrorContent from './encryption-error-content'
import { INFINITE_MESSAGES, InfiniteMessagesIds } from './infinite-messages'

export {
EncryptionErrorContent,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { InfiniteMessagesIds, INFINITE_MESSAGES } from './InfiniteMessages'

export { InfiniteMessagesIds, INFINITE_MESSAGES }
28 changes: 28 additions & 0 deletions redisinsight/ui/src/components/notifications/error-messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react'
import { EuiTextColor } from '@elastic/eui'
import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'
import { EncryptionErrorContent, DefaultErrorContent } from './components'
import CloudCapiUnAuthorizedErrorContent from './components/cloud-capi-unauthorized'

// TODO: use i18n file for texts
export default {
Expand Down Expand Up @@ -32,4 +33,31 @@ export default {
),
text: <EncryptionErrorContent instanceId={instanceId} onClose={onClose} />,
}),
CLOUD_CAPI_KEY_UNAUTHORIZED: (
{ id, message, title }: {
id: string,
message: string | JSX.Element,
title?: string
},
additionalInfo: Record<string, any>,
onClose?: () => void
): Toast => ({
id,
'data-test-subj': 'toast-error-cloud-capi-key-unauthorized',
color: 'danger',
iconType: 'alert',
onClose,
title: (
<EuiTextColor color="ghost">
<b>{title}</b>
</EuiTextColor>
),
text: (
<CloudCapiUnAuthorizedErrorContent
text={message}
resourceId={additionalInfo.resourceId}
onClose={onClose}
/>
),
})
}
4 changes: 3 additions & 1 deletion redisinsight/ui/src/components/notifications/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './Notifications'
import Notifications from './Notifications'

export default Notifications
Original file line number Diff line number Diff line change
Expand Up @@ -214,5 +214,13 @@ export default {
has been added.
</>
),
}),
REMOVED_ALL_CAPI_KEYS: () => ({
title: 'API keys have been removed',
message: 'All API keys have been removed from RedisInsight.',
}),
REMOVED_CAPI_KEY: (name: string) => ({
title: 'API Key has been removed',
message: `${formatNameShort(name)} has been removed from RedisInsight.`
})
}
2 changes: 2 additions & 0 deletions redisinsight/ui/src/components/oauth/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import OAuthSignInDialog from './oauth-sign-in-dialog'
import OAuthSocial, { OAuthSocialType } from './oauth-social'
import OAuthSocialDialog from './oauth-social-dialog'
import OAuthSocialHandlerDialog from './oauth-social-handler-dialog/OAuthSocialHandlerDialog'
import OAuthSsoHandlerDialog from './oauth-sso-handler-dialog/OAuthSsoHandlerDialog'
import OAuthSelectAccountDialog from './oauth-select-account-dialog/OAuthSelectAccountDialog'
import OAuthConnectFreeDb from './oauth-connect-free-db'
Expand All @@ -11,6 +12,7 @@ export {
OAuthSignInDialog,
OAuthSocial,
OAuthSocialDialog,
OAuthSocialHandlerDialog,
OAuthSsoHandlerDialog,
OAuthSelectAccountDialog,
OAuthConnectFreeDb,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
import React from 'react'
import { EuiModal, EuiModalBody } from '@elastic/eui'

import { useDispatch, useSelector } from 'react-redux'
import OAuthSocial, { OAuthSocialType } from 'uiSrc/components/oauth/oauth-social/OAuthSocial'

import { oauthCloudSelector, setSocialDialogState } from 'uiSrc/slices/oauth/cloud'
import styles from './styles.module.scss'

export interface Props {
onClose: () => void
}
const OAuthSocialDialog = () => {
const { isOpenSocialDialog } = useSelector(oauthCloudSelector)

const dispatch = useDispatch()

const handleClose = () => {
dispatch(setSocialDialogState(null))
}

const OAuthSocialDialog = ({ onClose }: Props) => (
<EuiModal onClose={() => onClose?.()} className={styles.modal} data-testid="social-oauth-dialog">
<EuiModalBody>
<OAuthSocial type={OAuthSocialType.Autodiscovery} />
</EuiModalBody>
</EuiModal>
)
if (!isOpenSocialDialog) {
return null
}

return (
<EuiModal onClose={handleClose} className={styles.modal} data-testid="social-oauth-dialog">
<EuiModalBody>
<OAuthSocial type={OAuthSocialType.Autodiscovery} />
</EuiModalBody>
</EuiModal>
)
}

export default OAuthSocialDialog
Loading