diff --git a/redisinsight/ui/src/components/code-block/CodeBlock.spec.tsx b/redisinsight/ui/src/components/code-block/CodeBlock.spec.tsx
new file mode 100644
index 0000000000..ff8a20180f
--- /dev/null
+++ b/redisinsight/ui/src/components/code-block/CodeBlock.spec.tsx
@@ -0,0 +1,46 @@
+import React from 'react'
+import { render, screen, fireEvent } from 'uiSrc/utils/test-utils'
+
+import CodeBlock from './CodeBlock'
+
+const originalClipboard = { ...global.navigator.clipboard }
+describe('CodeBlock', () => {
+ beforeEach(() => {
+ // @ts-ignore
+ global.navigator.clipboard = {
+ writeText: jest.fn(),
+ }
+ })
+
+ afterEach(() => {
+ jest.resetAllMocks()
+ // @ts-ignore
+ global.navigator.clipboard = originalClipboard
+ })
+
+ it('should render', () => {
+ expect(render(text)).toBeTruthy()
+ })
+
+ it('should render proper content', () => {
+ render(text)
+ expect(screen.getByTestId('code')).toHaveTextContent('text')
+ })
+
+ it('should not render copy button by default', () => {
+ render(text)
+ expect(screen.queryByTestId('copy-code-btn')).not.toBeInTheDocument()
+ })
+
+ it('should copy proper text', () => {
+ render(text)
+ fireEvent.click(screen.getByTestId('copy-code-btn'))
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith('text')
+ })
+
+ it('should copy proper text when children is ReactNode', () => {
+ render(text2)
+ fireEvent.click(screen.getByTestId('copy-code-btn'))
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith('text2')
+ })
+})
diff --git a/redisinsight/ui/src/components/code-block/CodeBlock.tsx b/redisinsight/ui/src/components/code-block/CodeBlock.tsx
new file mode 100644
index 0000000000..65a90e85f6
--- /dev/null
+++ b/redisinsight/ui/src/components/code-block/CodeBlock.tsx
@@ -0,0 +1,42 @@
+import React, { HTMLAttributes, useMemo } from 'react'
+import cx from 'classnames'
+import { EuiButtonIcon, useInnerText } from '@elastic/eui'
+
+import styles from './styles.module.scss'
+
+export interface Props extends HTMLAttributes {
+ children: React.ReactNode
+ className?: string
+ isCopyable?: boolean
+}
+
+const CodeBlock = (props: Props) => {
+ const { isCopyable, className, children, ...rest } = props
+ const [innerTextRef, innerTextString] = useInnerText('')
+
+ const innerText = useMemo(
+ () => innerTextString?.replace(/[\r\n?]{2}|\n\n/g, '\n') || '',
+ [innerTextString]
+ )
+
+ const handleCopyClick = () => {
+ navigator?.clipboard?.writeText(innerText)
+ }
+
+ return (
+
+
{children}
+ {isCopyable && (
+
+ )}
+
+ )
+}
+
+export default CodeBlock
diff --git a/redisinsight/ui/src/components/code-block/index.ts b/redisinsight/ui/src/components/code-block/index.ts
new file mode 100644
index 0000000000..b9022debf8
--- /dev/null
+++ b/redisinsight/ui/src/components/code-block/index.ts
@@ -0,0 +1,3 @@
+import CodeBlock from './CodeBlock'
+
+export default CodeBlock
diff --git a/redisinsight/ui/src/components/code-block/styles.module.scss b/redisinsight/ui/src/components/code-block/styles.module.scss
new file mode 100644
index 0000000000..7e6a6aa2a4
--- /dev/null
+++ b/redisinsight/ui/src/components/code-block/styles.module.scss
@@ -0,0 +1,19 @@
+.wrapper {
+ position: relative;
+
+ &.isCopyable {
+ .pre {
+ padding: 8px 30px 8px 16px !important;
+ }
+ }
+
+ .pre {
+ padding: 8px 16px !important;
+ }
+
+ .copyBtn {
+ position: absolute;
+ top: 4px;
+ right: 4px;
+ }
+}
diff --git a/redisinsight/ui/src/components/index.ts b/redisinsight/ui/src/components/index.ts
index 8177a6a37e..ee5d165e3f 100644
--- a/redisinsight/ui/src/components/index.ts
+++ b/redisinsight/ui/src/components/index.ts
@@ -21,6 +21,7 @@ import PagePlaceholder from './page-placeholder'
import BulkActionsConfig from './bulk-actions-config'
import ImportDatabasesDialog from './import-databases-dialog'
import OnboardingTour from './onboarding-tour'
+import CodeBlock from './code-block'
export {
NavigationMenu,
@@ -48,5 +49,6 @@ export {
PagePlaceholder,
BulkActionsConfig,
ImportDatabasesDialog,
- OnboardingTour
+ OnboardingTour,
+ CodeBlock,
}
diff --git a/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.spec.tsx b/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.spec.tsx
index 98ed9df539..91758b85c2 100644
--- a/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.spec.tsx
+++ b/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.spec.tsx
@@ -15,6 +15,9 @@ import { Pages } from 'uiSrc/constants'
import { setWorkbenchEAMinimized } from 'uiSrc/slices/app/context'
import { dbAnalysisSelector, setDatabaseAnalysisViewTab } from 'uiSrc/slices/analytics/dbAnalysis'
import { DatabaseAnalysisViewTab } from 'uiSrc/slices/interfaces/analytics'
+import { fetchRedisearchListAction, loadList } from 'uiSrc/slices/browser/redisearch'
+import { stringToBuffer } from 'uiSrc/utils'
+import { RedisResponseBuffer } from 'uiSrc/slices/interfaces'
import { ONBOARDING_FEATURES } from './OnboardingFeatures'
jest.mock('uiSrc/slices/app/features', () => ({
@@ -33,6 +36,12 @@ jest.mock('uiSrc/slices/browser/keys', () => ({
})
}))
+jest.mock('uiSrc/slices/browser/redisearch', () => ({
+ ...jest.requireActual('uiSrc/slices/browser/redisearch'),
+ fetchRedisearchListAction: jest.fn()
+ .mockImplementation(jest.requireActual('uiSrc/slices/browser/redisearch').fetchRedisearchListAction)
+}))
+
jest.mock('uiSrc/slices/analytics/dbAnalysis', () => ({
...jest.requireActual('uiSrc/slices/analytics/dbAnalysis'),
dbAnalysisSelector: jest.fn().mockReturnValue({
@@ -341,12 +350,40 @@ describe('ONBOARDING_FEATURES', () => {
checkAllTelemetryButtons(OnboardingStepName.WorkbenchIntro, sendEventTelemetry as jest.Mock)
})
+ it('should call proper actions on mount', () => {
+ render()
+
+ const expectedActions = [loadList()]
+ expect(clearStoreActions(store.getActions())).toEqual(clearStoreActions(expectedActions))
+ })
+
+ it('should render FT.INFO when there are indexes in database', () => {
+ const fetchRedisearchListActionMock = (onSuccess?: (indexes: RedisResponseBuffer[]) => void) =>
+ jest.fn().mockImplementation(() => onSuccess?.([stringToBuffer('someIndex')]));
+
+ (fetchRedisearchListAction as jest.Mock).mockImplementation(fetchRedisearchListActionMock)
+ render()
+
+ expect(screen.getByTestId('wb-onboarding-command')).toHaveTextContent('FT.INFO someIndex')
+ })
+
+ it('should render CLIENT LIST when there are no indexes in database', () => {
+ const fetchRedisearchListActionMock = (onSuccess?: (indexes: RedisResponseBuffer[]) => void) =>
+ jest.fn().mockImplementation(() => onSuccess?.([]));
+
+ (fetchRedisearchListAction as jest.Mock).mockImplementation(fetchRedisearchListActionMock)
+ render()
+
+ expect(screen.getByTestId('wb-onboarding-command')).toHaveTextContent('CLIENT LIST')
+ })
+
it('should call proper actions on back', () => {
render()
fireEvent.click(screen.getByTestId('back-btn'))
const expectedActions = [showMonitor(), setOnboardPrevStep()]
- expect(clearStoreActions(store.getActions())).toEqual(clearStoreActions(expectedActions))
+ expect(clearStoreActions(store.getActions().slice(-2)))
+ .toEqual(clearStoreActions(expectedActions))
})
it('should properly push history on back', () => {
diff --git a/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.tsx b/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.tsx
index cdfeaf5e98..b6578e258a 100644
--- a/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.tsx
+++ b/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.tsx
@@ -1,8 +1,8 @@
-import React, { useEffect } from 'react'
+import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useHistory } from 'react-router-dom'
import { EuiIcon, EuiSpacer } from '@elastic/eui'
-import { partialRight } from 'lodash'
+import { isString, partialRight } from 'lodash'
import { keysDataSelector } from 'uiSrc/slices/browser/keys'
import { openCli, openCliHelper, resetCliHelperSettings, resetCliSettings } from 'uiSrc/slices/cli/cli-settings'
import { setMonitorInitialState, showMonitor } from 'uiSrc/slices/cli/monitor'
@@ -17,6 +17,9 @@ import OnboardingEmoji from 'uiSrc/assets/img/onboarding-emoji.svg'
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
import { OnboardingStepName, OnboardingSteps } from 'uiSrc/constants/onboarding'
+import { fetchRedisearchListAction } from 'uiSrc/slices/browser/redisearch'
+import { bufferToString, Nullable } from 'uiSrc/utils'
+import { CodeBlock } from 'uiSrc/components'
import styles from './styles.module.scss'
const sendTelemetry = (databaseId: string, step: string, action: string) => sendEventTelemetry({
@@ -185,11 +188,22 @@ const ONBOARDING_FEATURES = {
title: 'Try Workbench!',
Inner: () => {
const { id: connectedInstanceId = '' } = useSelector(connectedInstanceSelector)
+ const [firstIndex, setFirstIndex] = useState>(null)
const dispatch = useDispatch()
const history = useHistory()
const telemetryArgs: TelemetryArgs = [connectedInstanceId, OnboardingStepName.WorkbenchIntro]
+ useEffect(() => {
+ dispatch(fetchRedisearchListAction(
+ (indexes) => {
+ setFirstIndex(indexes?.length ? bufferToString(indexes[0]) : '')
+ },
+ undefined,
+ false
+ ))
+ }, [])
+
return {
content: (
<>
@@ -201,10 +215,36 @@ const ONBOARDING_FEATURES = {
models such as documents, graphs, and time series.
Or you can build your own visualization.
-
- Run this command to see information and statistics about client connections:
-
- CLIENT LIST
+ {isString(firstIndex) && (
+ <>
+
+ {firstIndex ? (
+ <>
+ Run this command to see information and statistics on your index:
+
+
+ FT.INFO {firstIndex}
+
+ >
+ ) : (
+ <>
+ Run this command to see information and statistics about client connections:
+
+
+ CLIENT LIST
+
+ >
+ )}
+ >
+ )}
>
),
onSkip: () => sendClosedTelemetryEvent(...telemetryArgs),
diff --git a/redisinsight/ui/src/components/onboarding-features/styles.module.scss b/redisinsight/ui/src/components/onboarding-features/styles.module.scss
index 052d99056d..7a14a1e177 100644
--- a/redisinsight/ui/src/components/onboarding-features/styles.module.scss
+++ b/redisinsight/ui/src/components/onboarding-features/styles.module.scss
@@ -1,4 +1,12 @@
+@import '@elastic/eui/src/global_styling/mixins/helpers';
+@import '@elastic/eui/src/components/table/mixins';
+@import '@elastic/eui/src/global_styling/index';
+
.pre {
- padding: 8px 16px !important;
background-color: var(--commandGroupBadgeColor) !important;
+ word-wrap: break-word;
+
+ max-height: 240px;
+ overflow-y: auto;
+ @include euiScrollBar;
}
diff --git a/tests/e2e/common-actions/onboard-actions.ts b/tests/e2e/common-actions/onboard-actions.ts
index 9ff7d27278..0725ecb4ba 100644
--- a/tests/e2e/common-actions/onboard-actions.ts
+++ b/tests/e2e/common-actions/onboard-actions.ts
@@ -27,7 +27,7 @@ export class OnboardActions {
complete onboarding process
*/
async verifyOnboardingCompleted(): Promise {
- await t.expect(onboardingPage.showMeAroundButton.visible).notOk('show me around button still visible');
+ await t.expect(onboardingPage.showMeAroundButton.exists).notOk('show me around button still visible');
await t.expect(browserPage.patternModeBtn.visible).ok('browser page is not opened');
}
/**
diff --git a/tests/e2e/helpers/conf.ts b/tests/e2e/helpers/conf.ts
index fad758adba..7ddafc8834 100644
--- a/tests/e2e/helpers/conf.ts
+++ b/tests/e2e/helpers/conf.ts
@@ -1,7 +1,7 @@
-import { Chance } from 'chance';
import * as os from 'os';
import * as fs from 'fs';
import { join as joinPath } from 'path';
+import { Chance } from 'chance';
const chance = new Chance();
// Urls for using in the tests
@@ -19,6 +19,14 @@ export const ossStandaloneConfig = {
databasePassword: process.env.OSS_STANDALONE_PASSWORD
};
+export const ossStandaloneConfigEmpty = {
+ host: process.env.OSS_STANDALONE_HOST || 'oss-standalone-empty',
+ port: process.env.OSS_STANDALONE_PORT || '6379',
+ databaseName: `${process.env.OSS_STANDALONE_DATABASE_NAME || 'test_standalone_empty'}-${uniqueId}`,
+ databaseUsername: process.env.OSS_STANDALONE_USERNAME,
+ databasePassword: process.env.OSS_STANDALONE_PASSWORD
+};
+
export const ossStandaloneV5Config = {
host: process.env.OSS_STANDALONE_V5_HOST || 'oss-standalone-v5',
port: process.env.OSS_STANDALONE_V5_PORT || '6379',
diff --git a/tests/e2e/pageObjects/onboarding-page.ts b/tests/e2e/pageObjects/onboarding-page.ts
index ef6d96957b..aa711e3422 100644
--- a/tests/e2e/pageObjects/onboarding-page.ts
+++ b/tests/e2e/pageObjects/onboarding-page.ts
@@ -6,4 +6,6 @@ export class OnboardingPage {
showMeAroundButton = Selector('span').withText('Show me around');
skipTourButton = Selector('[data-testid=skip-tour-btn]');
stepTitle = Selector('[data-testid=step-title]');
+ wbOnbardingCommand = Selector('[data-testid=wb-onboarding-command]');
+ copyCodeButton = Selector('[data-testid=copy-code-btn]');
}
diff --git a/tests/e2e/rte.docker-compose.yml b/tests/e2e/rte.docker-compose.yml
index 309ed2276f..e2b75277a6 100644
--- a/tests/e2e/rte.docker-compose.yml
+++ b/tests/e2e/rte.docker-compose.yml
@@ -33,6 +33,18 @@ services:
ports:
- 8100:6379
+ oss-standalone-empty:
+ image: redislabs/redismod
+ command: [
+ "--loadmodule", "/usr/lib/redis/modules/redisearch.so",
+ "--loadmodule", "/usr/lib/redis/modules/redisgraph.so",
+ "--loadmodule", "/usr/lib/redis/modules/redistimeseries.so",
+ "--loadmodule", "/usr/lib/redis/modules/rejson.so",
+ "--loadmodule", "/usr/lib/redis/modules/redisbloom.so"
+ ]
+ ports:
+ - 8105:6379
+
# oss standalone v5
oss-standalone-v5:
image: redis:5
diff --git a/tests/e2e/tests/regression/browser/onboarding.e2e.ts b/tests/e2e/tests/regression/browser/onboarding.e2e.ts
index 4233043f16..a597e0f261 100644
--- a/tests/e2e/tests/regression/browser/onboarding.e2e.ts
+++ b/tests/e2e/tests/regression/browser/onboarding.e2e.ts
@@ -3,15 +3,24 @@ import {
acceptTermsAddDatabaseOrConnectToRedisStack, deleteDatabase
} from '../../../helpers/database';
import {
- commonUrl, ossStandaloneConfig
+ commonUrl, ossStandaloneConfigEmpty
} from '../../../helpers/conf';
import { env, rte } from '../../../helpers/constants';
import {Common} from '../../../helpers/common';
import {OnboardActions} from '../../../common-actions/onboard-actions';
-import {CliPage, MemoryEfficiencyPage, SlowLogPage, WorkbenchPage, PubSubPage, MonitorPage} from '../../../pageObjects';
+import {
+ CliPage,
+ MemoryEfficiencyPage,
+ SlowLogPage,
+ WorkbenchPage,
+ PubSubPage,
+ MonitorPage,
+ OnboardingPage
+} from '../../../pageObjects';
const common = new Common();
const onBoardActions = new OnboardActions();
+const onboardingPage = new OnboardingPage();
const cliPage = new CliPage();
const memoryEfficiencyPage = new MemoryEfficiencyPage();
const workBenchPage = new WorkbenchPage();
@@ -19,18 +28,21 @@ const slowLogPage = new SlowLogPage();
const pubSubPage = new PubSubPage();
const monitorPage = new MonitorPage();
const setLocalStorageItem = ClientFunction((key: string, value: string) => window.localStorage.setItem(key, value));
+const indexName = common.generateWord(10);
fixture `Onboarding new user tests`
.meta({type: 'regression', rte: rte.standalone })
.page(commonUrl)
.beforeEach(async() => {
- await acceptTermsAddDatabaseOrConnectToRedisStack(ossStandaloneConfig, ossStandaloneConfig.databaseName);
+ await acceptTermsAddDatabaseOrConnectToRedisStack(ossStandaloneConfigEmpty, ossStandaloneConfigEmpty.databaseName);
await setLocalStorageItem('onboardingStep', '0');
await common.reloadPage();
})
.afterEach(async() => {
- await deleteDatabase(ossStandaloneConfig.databaseName);
+ await cliPage.sendCommandInCli(`DEL ${indexName}`);
+ await deleteDatabase(ossStandaloneConfigEmpty.databaseName);
});
+// https://redislabs.atlassian.net/browse/RI-4070, https://redislabs.atlassian.net/browse/RI-4067
test
.meta({ env: env.desktop })('Verify onbarding new user steps', async t => {
await onBoardActions.startOnboarding();
@@ -55,16 +67,23 @@ test
await t.expect(monitorPage.monitorArea.visible).ok('profiler is not expanded');
await onBoardActions.verifyStepVisible('Profiler');
await onBoardActions.clickNextStep();
+ // Verify that client list command visible when there is not any index created
+ await t.expect(onboardingPage.wbOnbardingCommand.withText('CLIENT LIST').visible).ok('CLIENT LIST command is not visible');
+ await t.expect(onboardingPage.copyCodeButton.visible).ok('copy code button is not visible');
// verify workbench page is opened
await t.expect(workBenchPage.mainEditorArea.visible).ok('workbench is not opened');
await onBoardActions.verifyStepVisible('Try Workbench!');
// click back step button
await onBoardActions.clickBackStep();
+ // create index in order to see in FT.INFO {index} in onboarding step
+ await cliPage.sendCommandInCli(`FT.CREATE ${indexName} ON HASH PREFIX 1 test SCHEMA "name" TEXT`);
// verify one step before is opened
await t.expect(monitorPage.monitorArea.visible).ok('profiler is not expanded');
await onBoardActions.verifyStepVisible('Profiler');
await onBoardActions.clickNextStep();
// verify workbench page is opened
+ await t.expect(onboardingPage.wbOnbardingCommand.withText(`FT.INFO ${indexName}`).visible).ok(`FT.INFO ${indexName} command is not visible`);
+ await t.expect(onboardingPage.copyCodeButton.visible).ok('copy code button is not visible');
await t.expect(workBenchPage.mainEditorArea.visible).ok('workbench is not opened');
await onBoardActions.verifyStepVisible('Try Workbench!');
await onBoardActions.clickNextStep();
@@ -75,7 +94,7 @@ test
await onBoardActions.verifyStepVisible('Database Analysis');
await onBoardActions.clickNextStep();
// verify slow log is opened
- await t.expect(slowLogPage.slowLogTable.visible).ok('slow log is not opened');
+ await t.expect(slowLogPage.slowLogConfigureButton.visible).ok('slow log is not opened');
await onBoardActions.verifyStepVisible('Slow Log');
await onBoardActions.clickNextStep();
// verify pub/sub page is opened
@@ -88,6 +107,7 @@ test
// verify onboarding step completed successfully
await onBoardActions.verifyOnboardingCompleted();
});
+// https://redislabs.atlassian.net/browse/RI-4067
test
.meta({ env: env.desktop })('verify onboard new user skip tour', async() => {
// start onboarding process