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
22 changes: 22 additions & 0 deletions redisinsight/ui/src/assets/img/alarm.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ import { setSocialDialogState } from 'uiSrc/slices/oauth/cloud'
import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'
import { getUtmExternalLink } from 'uiSrc/utils/links'
import { CREATE_CLOUD_DB_ID, HELP_LINKS } from 'uiSrc/pages/home/constants'

import DbStatus from '../db-status'

import styles from './styles.module.scss'

export interface Props {
Expand Down Expand Up @@ -295,16 +298,18 @@ const DatabasesListWrapper = (props: Props) => {
)
}

const { id, db, new: newStatus = false } = instance
const { id, db, new: newStatus = false, lastConnection, createdAt, cloudDetails } = instance
const cellContent = replaceSpaces(name.substring(0, 200))

return (
<div role="presentation">
{newStatus && (
<EuiToolTip content="New" position="top" anchorClassName={styles.newStatusAnchor}>
<div className={styles.newStatus} data-testid={`database-status-new-${id}`} />
</EuiToolTip>
)}
<DbStatus
id={id}
isNew={newStatus}
lastConnection={lastConnection}
createdAt={createdAt}
isFree={cloudDetails?.free}
/>
<EuiToolTip
position="bottom"
title="Database Alias"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,21 +76,6 @@ $breakpoint-l: 1400px;
}
}

.newStatus {
background-color: var(--euiColorPrimary) !important;
cursor: pointer;
width: 11px !important;
min-width: 11px !important;
height: 11px !important;
border-radius: 6px;
}

.newStatusAnchor {
margin-top: 20px;
margin-left: -19px;
position: absolute;
}

.container {
// Database alias column
height: 100%;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from 'react'
import { mock } from 'ts-mockito'
import { act, fireEvent, render, screen, waitForEuiToolTipVisible } from 'uiSrc/utils/test-utils'

import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
import DbStatus, { Props, WarningTypes } from './DbStatus'

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

const mockedProps = mock<Props>()
const daysToMs = (days: number) => days * 60 * 60 * 24 * 1000

describe('DbStatus', () => {
it('should render', () => {
expect(render(<DbStatus {...mockedProps} />)).toBeTruthy()
})

it('should not render any status', () => {
render(<DbStatus {...mockedProps} id="1" />)

expect(screen.queryByTestId('database-status-new-1')).not.toBeInTheDocument()
expect(screen.queryByTestId(`database-status-${WarningTypes.TryDatabase}-1`)).not.toBeInTheDocument()
expect(screen.queryByTestId(`database-status-${WarningTypes.CheckIfDeleted}-1`)).not.toBeInTheDocument()
})

it('should render TryDatabase status', () => {
const lastConnection = new Date(Date.now() - daysToMs(3))
render(<DbStatus {...mockedProps} id="1" lastConnection={lastConnection} isFree />)

expect(screen.getByTestId(`database-status-${WarningTypes.TryDatabase}-1`)).toBeInTheDocument()
expect(screen.queryByTestId('database-status-new-1')).not.toBeInTheDocument()
expect(screen.queryByTestId(`database-status-${WarningTypes.CheckIfDeleted}-1`)).not.toBeInTheDocument()
})

it('should render CheckIfDeleted status', () => {
const lastConnection = new Date(Date.now() - daysToMs(16))
render(<DbStatus {...mockedProps} id="1" lastConnection={lastConnection} isFree isNew />)

expect(screen.getByTestId(`database-status-${WarningTypes.CheckIfDeleted}-1`)).toBeInTheDocument()

expect(screen.queryByTestId('database-status-new-1')).not.toBeInTheDocument()
expect(screen.queryByTestId(`database-status-${WarningTypes.TryDatabase}-1`)).not.toBeInTheDocument()
})

it('should render new status', async () => {
const sendEventTelemetryMock = jest.fn();
(sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock)

const lastConnection = new Date(Date.now() - daysToMs(3))
render(<DbStatus {...mockedProps} id="1" lastConnection={lastConnection} isFree />)

await act(async () => {
fireEvent.mouseOver(screen.getByTestId(`database-status-${WarningTypes.TryDatabase}-1`))
})

await waitForEuiToolTipVisible(1_000)

expect(sendEventTelemetry).toBeCalledWith({
event: TelemetryEvent.CLOUD_NOT_USED_DB_NOTIFICATION_VIEWED,
eventData: {
capability: expect.any(String),
databaseId: '1',
type: WarningTypes.TryDatabase
}
})
})
})
119 changes: 119 additions & 0 deletions redisinsight/ui/src/pages/home/components/db-status/DbStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React from 'react'
import { EuiIcon, EuiToolTip } from '@elastic/eui'
import cx from 'classnames'
import { differenceInDays } from 'date-fns'

import { useSelector } from 'react-redux'
import { getTutorialCapability, Maybe } from 'uiSrc/utils'

import { appContextCapability } from 'uiSrc/slices/app/context'

import AlarmIcon from 'uiSrc/assets/img/alarm.svg'
import { isShowCapabilityTutorialPopover } from 'uiSrc/services'
import { sendEventTelemetry, TELEMETRY_EMPTY_VALUE, TelemetryEvent } from 'uiSrc/telemetry'
import { CHECK_CLOUD_DATABASE, WARNING_WITH_CAPABILITY, WARNING_WITHOUT_CAPABILITY } from './texts'
import styles from './styles.module.scss'

export interface Props {
id: string
lastConnection: Maybe<Date>
createdAt: Maybe<Date>
isNew: boolean
isFree?: boolean
}

export enum WarningTypes {
CheckIfDeleted = 'checkIfDeleted',
TryDatabase = 'tryDatabase',
}

interface WarningTooltipProps {
id: string
content : React.ReactNode
capabilityTelemetry?: string
type?: string
isCapabilityNotShown?: boolean
}

const LAST_CONNECTION_SM = 3
const LAST_CONNECTION_L = 16

const DbStatus = (props: Props) => {
const { id, lastConnection, createdAt, isNew, isFree } = props

const { source } = useSelector(appContextCapability)
const capability = getTutorialCapability(source!)
const isCapabilityNotShown = Boolean(isShowCapabilityTutorialPopover(isFree))
let daysDiff = 0

try {
daysDiff = lastConnection
? differenceInDays(new Date(), new Date(lastConnection))
: createdAt ? differenceInDays(new Date(), new Date(createdAt)) : 0
} catch {
// nothing to do
}

const renderWarningTooltip = (content: React.ReactNode, type?: string) => (
<EuiToolTip
content={(
<WarningTooltipContent
id={id}
capabilityTelemetry={capability?.telemetryName}
content={content}
type={type}
isCapabilityNotShown={isCapabilityNotShown}
/>
)}
position="right"
className={styles.tooltip}
anchorClassName={cx(styles.statusAnchor, styles.warning)}
>
<div className={cx(styles.status, styles.warning)} data-testid={`database-status-${type}-${id}`}>!</div>
</EuiToolTip>
)

if (isFree && daysDiff >= LAST_CONNECTION_L) {
return renderWarningTooltip(CHECK_CLOUD_DATABASE, 'checkIfDeleted')
}

if (isFree && daysDiff >= LAST_CONNECTION_SM) {
return renderWarningTooltip(
isCapabilityNotShown && capability.name ? WARNING_WITH_CAPABILITY(capability.name) : WARNING_WITHOUT_CAPABILITY,
'tryDatabase'
)
}

if (isNew) {
return (
<EuiToolTip content="New" position="top" anchorClassName={cx(styles.statusAnchor)}>
<div className={cx(styles.status, styles.new)} data-testid={`database-status-new-${id}`} />
</EuiToolTip>
)
}

return null
}

// separated to send event when content is displayed
const WarningTooltipContent = (props: WarningTooltipProps) => {
const { id, content, capabilityTelemetry, type, isCapabilityNotShown } = props

sendEventTelemetry({
event: TelemetryEvent.CLOUD_NOT_USED_DB_NOTIFICATION_VIEWED,
eventData: {
databaseId: id,
capability: isCapabilityNotShown ? capabilityTelemetry : TELEMETRY_EMPTY_VALUE,
type
}
})

return (
<div className={styles.warningTooltipContent}>
<EuiIcon type={AlarmIcon} size="original" />
<div>{content}</div>
</div>
)
}

export default DbStatus
3 changes: 3 additions & 0 deletions redisinsight/ui/src/pages/home/components/db-status/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import DbStatus from './DbStatus'

export default DbStatus
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
.status {
cursor: pointer;
width: 11px !important;
min-width: 11px !important;
height: 11px !important;
border-radius: 50%;

&.new {
background-color: var(--euiColorPrimary) !important;
}

&.warning {
width: 14px !important;
height: 14px !important;
background-color: var(--euiColorWarningLight) !important;

line-height: 14px !important;
text-align: center;
color: var(--euiColorEmptyShade);
font-size: 11px;
}
}

.statusAnchor {
margin-top: 20px;
margin-left: -19px;
position: absolute;

&.warning {
margin-top: 17px;
margin-left: -21px;
}
}

.tooltip {
min-width: 340px !important;
}

.warningTooltipContent {
display: flex;
align-items: flex-start;

:global(.euiIcon) {
margin-top: 4px;
margin-right: 12px;
}
}
37 changes: 37 additions & 0 deletions redisinsight/ui/src/pages/home/components/db-status/texts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { EuiSpacer, EuiTitle } from '@elastic/eui'
import React from 'react'

export const CHECK_CLOUD_DATABASE = (
<>
<EuiTitle size="xxs"><span>Check your Cloud database</span></EuiTitle>
<EuiSpacer size="s" />
<div>
Free Cloud databases are usually deleted after 15 days of inactivity.
<EuiSpacer size="s" />
Check your Cloud database to proceed with learning more about Redis and its capabilities.
</div>
</>
)

export const WARNING_WITH_CAPABILITY = (capability: string) => (
<>
<EuiTitle size="xxs"><span>{capability}</span></EuiTitle>
<EuiSpacer size="s" />
<div>
Hey, remember you expressed interest in {capability}?
<br />
Try your Cloud database to get started.
</div>
<EuiSpacer size="s" />
<div><b>Notice</b>: free Cloud databases will be deleted after 15 days of inactivity.</div>
</>
)
export const WARNING_WITHOUT_CAPABILITY = (
<>
<EuiTitle size="xxs"><span>Try your Cloud database</span></EuiTitle>
<EuiSpacer size="s" />
<div>Hey, try your Cloud database to learn more about Redis.</div>
<EuiSpacer size="s" />
<div><b>Notice</b>: free Cloud databases will be deleted after 15 days of inactivity.</div>
</>
)
1 change: 1 addition & 0 deletions redisinsight/ui/src/telemetry/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ export enum TelemetryEvent {
CLOUD_ACCOUNT_SWITCHED = 'CLOUD_ACCOUNT_SWITCHED',
CLOUD_CONSOLE_CLICKED = 'CLOUD_CONSOLE_CLICKED',
CLOUD_SIGN_OUT_CLICKED = 'CLOUD_SIGN_OUT_CLICKED',
CLOUD_NOT_USED_DB_NOTIFICATION_VIEWED = 'CLOUD_NOT_USED_DB_NOTIFICATION_VIEWED',

RDI_INSTANCE_LIST_SORTED = 'RDI_INSTANCE_LIST_SORTED',
RDI_INSTANCE_SINGLE_DELETE_CLICKED = 'RDI_INSTANCE_SINGLE_DELETE_CLICKED',
Expand Down
4 changes: 2 additions & 2 deletions redisinsight/ui/src/utils/capability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const getTutorialCapability = (source: any = '') => {
case getSourceTutorialByCapability(RedisDefaultModules.FTL):
return getCapability(
'searchAndQuery',
'Redis Query Engine capability',
'Redis Query Engine',
findMarkdownPath(store.getState()?.workbench?.tutorials?.items, { id: 'sq-intro' })
)

Expand All @@ -33,7 +33,7 @@ export const getTutorialCapability = (source: any = '') => {
case getSourceTutorialByCapability(RedisDefaultModules.ReJSON):
return getCapability(
'JSON',
'JSON capability',
'JSON data structure',
findMarkdownPath(store.getState()?.workbench?.tutorials?.items, { id: 'ds-json-intro' })
)

Expand Down
Loading
Loading