diff --git a/redisinsight/ui/src/assets/img/alarm.svg b/redisinsight/ui/src/assets/img/alarm.svg
new file mode 100644
index 0000000000..edce553e5b
--- /dev/null
+++ b/redisinsight/ui/src/assets/img/alarm.svg
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/redisinsight/ui/src/pages/home/components/database-list-component/DatabasesListWrapper.tsx b/redisinsight/ui/src/pages/home/components/database-list-component/DatabasesListWrapper.tsx
index 5ede92771e..f7c7111cdd 100644
--- a/redisinsight/ui/src/pages/home/components/database-list-component/DatabasesListWrapper.tsx
+++ b/redisinsight/ui/src/pages/home/components/database-list-component/DatabasesListWrapper.tsx
@@ -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 {
@@ -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 (
- {newStatus && (
-
-
-
- )}
+
({
+ ...jest.requireActual('uiSrc/telemetry'),
+ sendEventTelemetry: jest.fn(),
+}))
+
+const mockedProps = mock()
+const daysToMs = (days: number) => days * 60 * 60 * 24 * 1000
+
+describe('DbStatus', () => {
+ it('should render', () => {
+ expect(render( )).toBeTruthy()
+ })
+
+ it('should not render any status', () => {
+ render( )
+
+ 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( )
+
+ 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( )
+
+ 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( )
+
+ 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
+ }
+ })
+ })
+})
diff --git a/redisinsight/ui/src/pages/home/components/db-status/DbStatus.tsx b/redisinsight/ui/src/pages/home/components/db-status/DbStatus.tsx
new file mode 100644
index 0000000000..f093a0a223
--- /dev/null
+++ b/redisinsight/ui/src/pages/home/components/db-status/DbStatus.tsx
@@ -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
+ createdAt: Maybe
+ 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) => (
+
+ )}
+ position="right"
+ className={styles.tooltip}
+ anchorClassName={cx(styles.statusAnchor, styles.warning)}
+ >
+ !
+
+ )
+
+ 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 (
+
+
+
+ )
+ }
+
+ 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 (
+
+ )
+}
+
+export default DbStatus
diff --git a/redisinsight/ui/src/pages/home/components/db-status/index.ts b/redisinsight/ui/src/pages/home/components/db-status/index.ts
new file mode 100644
index 0000000000..b777907a83
--- /dev/null
+++ b/redisinsight/ui/src/pages/home/components/db-status/index.ts
@@ -0,0 +1,3 @@
+import DbStatus from './DbStatus'
+
+export default DbStatus
diff --git a/redisinsight/ui/src/pages/home/components/db-status/styles.module.scss b/redisinsight/ui/src/pages/home/components/db-status/styles.module.scss
new file mode 100644
index 0000000000..617fe08eb7
--- /dev/null
+++ b/redisinsight/ui/src/pages/home/components/db-status/styles.module.scss
@@ -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;
+ }
+}
diff --git a/redisinsight/ui/src/pages/home/components/db-status/texts.tsx b/redisinsight/ui/src/pages/home/components/db-status/texts.tsx
new file mode 100644
index 0000000000..466c15877e
--- /dev/null
+++ b/redisinsight/ui/src/pages/home/components/db-status/texts.tsx
@@ -0,0 +1,37 @@
+import { EuiSpacer, EuiTitle } from '@elastic/eui'
+import React from 'react'
+
+export const CHECK_CLOUD_DATABASE = (
+ <>
+
Check your Cloud database
+
+
+ Free Cloud databases are usually deleted after 15 days of inactivity.
+
+ Check your Cloud database to proceed with learning more about Redis and its capabilities.
+
+ >
+)
+
+export const WARNING_WITH_CAPABILITY = (capability: string) => (
+ <>
+
{capability}
+
+
+ Hey, remember you expressed interest in {capability}?
+
+ Try your Cloud database to get started.
+
+
+
Notice : free Cloud databases will be deleted after 15 days of inactivity.
+ >
+)
+export const WARNING_WITHOUT_CAPABILITY = (
+ <>
+
Try your Cloud database
+
+
Hey, try your Cloud database to learn more about Redis.
+
+
Notice : free Cloud databases will be deleted after 15 days of inactivity.
+ >
+)
diff --git a/redisinsight/ui/src/telemetry/events.ts b/redisinsight/ui/src/telemetry/events.ts
index 89d23ae97e..63c240b3f0 100644
--- a/redisinsight/ui/src/telemetry/events.ts
+++ b/redisinsight/ui/src/telemetry/events.ts
@@ -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',
diff --git a/redisinsight/ui/src/utils/capability.ts b/redisinsight/ui/src/utils/capability.ts
index 19371f3244..bda81a1298 100644
--- a/redisinsight/ui/src/utils/capability.ts
+++ b/redisinsight/ui/src/utils/capability.ts
@@ -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' })
)
@@ -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' })
)
diff --git a/redisinsight/ui/src/utils/test-utils.tsx b/redisinsight/ui/src/utils/test-utils.tsx
index f6464e8005..f11d7ae74b 100644
--- a/redisinsight/ui/src/utils/test-utils.tsx
+++ b/redisinsight/ui/src/utils/test-utils.tsx
@@ -186,13 +186,13 @@ const clearStoreActions = (actions: any[]) => {
/**
* Ensure the EuiToolTip being tested is open and visible before continuing
*/
-const waitForEuiToolTipVisible = async () => {
+const waitForEuiToolTipVisible = async (timeout = 500) => {
await waitFor(
() => {
const tooltip = document.querySelector('.euiToolTipPopover')
expect(tooltip).toBeInTheDocument()
},
- { timeout: 500 } // Account for long delay on tooltips
+ { timeout } // Account for long delay on tooltips
)
}
diff --git a/redisinsight/ui/src/utils/tests/capability.spec.ts b/redisinsight/ui/src/utils/tests/capability.spec.ts
index 962182a5ee..cd3148f7a7 100644
--- a/redisinsight/ui/src/utils/tests/capability.spec.ts
+++ b/redisinsight/ui/src/utils/tests/capability.spec.ts
@@ -16,8 +16,8 @@ describe('getSourceTutorialByCapability', () => {
})
const emptyCapability = { name: '', telemetryName: '', path: null, }
-const searchCapability = { name: 'Redis Query Engine capability', telemetryName: 'searchAndQuery', path: null, }
-const jsonCapability = { name: 'JSON capability', telemetryName: 'JSON', path: null, }
+const searchCapability = { name: 'Redis Query Engine', telemetryName: 'searchAndQuery', path: null, }
+const jsonCapability = { name: 'JSON data structure', telemetryName: 'JSON', path: null, }
const tsCapability = { name: 'Time series data structure', telemetryName: 'timeSeries', path: null, }
const bloomCapability = { name: 'Probabilistic data structures', telemetryName: 'probabilistic', path: null, }