diff --git a/redisinsight/ui/src/components/cli/components/cli-body/CliBody/CliBody.tsx b/redisinsight/ui/src/components/cli/components/cli-body/CliBody/CliBody.tsx index d7392503dd..5940c47cc0 100644 --- a/redisinsight/ui/src/components/cli/components/cli-body/CliBody/CliBody.tsx +++ b/redisinsight/ui/src/components/cli/components/cli-body/CliBody/CliBody.tsx @@ -23,6 +23,8 @@ export interface Props { const commandTabPosInit = 0 const commandHistoryPosInit = -1 +const TIME_FOR_DOUBLE_CLICK = 300 + const CliBody = (props: Props) => { const { data, command = '', error, setCommand, onSubmit } = props @@ -36,6 +38,7 @@ const CliBody = (props: Props) => { const { loading, commandHistory: commandHistoryStore } = useSelector(outputSelector) const { commandsArray } = useSelector(appRedisCommandsSelector) + const timerClickRef = useRef() const scrollDivRef: Ref = useRef(null) const dispatch = useDispatch() @@ -149,44 +152,23 @@ const CliBody = (props: Props) => { const isModifierKey = isModifiedEvent(event) - if (event.shiftKey && event.key === keys.TAB) { - onKeyDownShiftTab(event) - return - } - - if (event.key === keys.TAB) { - onKeyDownTab(event, commandLine) - return - } + if (event.shiftKey && event.key === keys.TAB) return onKeyDownShiftTab(event) + if (event.key === keys.TAB) return onKeyDownTab(event, commandLine) // reset command tab position if (!event.shiftKey || (event.shiftKey && event.key !== 'Shift')) { setCommandTabPos(commandTabPosInit) } - if (event.key === keys.ENTER) { - onKeyDownEnter(commandLine, event) - return - } - - if (event.key === keys.ARROW_UP && !isModifierKey) { - onKeyDownArrowUp(event) - return - } - - if (event.key === keys.ARROW_DOWN && !isModifierKey) { - onKeyDownArrowDown(event) - return - } - - if (event.key === keys.ESCAPE) { - onKeyEsc() - return - } + if (event.key === keys.ENTER) return onKeyDownEnter(commandLine, event) + if (event.key === keys.ARROW_UP && !isModifierKey) return onKeyDownArrowUp(event) + if (event.key === keys.ARROW_DOWN && !isModifierKey) return onKeyDownArrowDown(event) + if (event.key === keys.ESCAPE) return onKeyEsc() if ((event.metaKey && event.key === 'k') || (event.ctrlKey && event.key === 'l')) { onClearOutput(event) } + return undefined } const updateMatchingCmds = (command: string = '') => { @@ -212,15 +194,36 @@ const CliBody = (props: Props) => { } const onMouseUpOutput = () => { - if (!window.getSelection()?.toString()) { - inputEl?.focus() - document.execCommand('selectAll', false) - document.getSelection()?.collapseToEnd() + if (timerClickRef.current) { + clearTimeout(timerClickRef.current) + timerClickRef.current = undefined + return + } + + if (window.getSelection()?.toString()) { + return } + + timerClickRef.current = setTimeout(() => { + const isInputFocused = document.activeElement === inputEl + + if (!window.getSelection()?.toString() && !isInputFocused) { + inputEl?.focus() + document.execCommand('selectAll', false) + document.getSelection()?.collapseToEnd() + timerClickRef.current = undefined + } + }, TIME_FOR_DOUBLE_CLICK) } return ( -
+
{}} + role="textbox" + tabIndex={0} + > void; - onKeyDown: (event: React.KeyboardEvent) => void; - dbIndex: number; + command: string + setInputEl: Function + setCommand: (command: string) => void + onKeyDown: (event: React.KeyboardEvent) => void + dbIndex: number } const CliInput = (props: Props) => { diff --git a/redisinsight/ui/src/components/cli/components/cli-input/CliInputWrapper.tsx b/redisinsight/ui/src/components/cli/components/cli-input/CliInputWrapper.tsx index af13dded69..f01292e96a 100644 --- a/redisinsight/ui/src/components/cli/components/cli-input/CliInputWrapper.tsx +++ b/redisinsight/ui/src/components/cli/components/cli-input/CliInputWrapper.tsx @@ -9,11 +9,11 @@ import CliAutocomplete from './CliAutocomplete' import CliInput from './CliInput' export interface Props { - command: string; - wordsTyped: number; - setInputEl: Function; - setCommand: (command: string) => void; - onKeyDown: (event: React.KeyboardEvent) => void; + command: string + wordsTyped: number + setInputEl: Function + setCommand: (command: string) => void + onKeyDown: (event: React.KeyboardEvent) => void } const CliInputWrapper = (props: Props) => { diff --git a/redisinsight/ui/src/components/query/QueryWrapper.tsx b/redisinsight/ui/src/components/query/QueryWrapper.tsx index f1a981d4bc..e4d41ad5c0 100644 --- a/redisinsight/ui/src/components/query/QueryWrapper.tsx +++ b/redisinsight/ui/src/components/query/QueryWrapper.tsx @@ -1,3 +1,4 @@ +import { without } from 'lodash' import React from 'react' import { useSelector } from 'react-redux' import { EuiLoadingContent } from '@elastic/eui' @@ -63,7 +64,11 @@ const QueryWrapper = (props: Props) => { const sendEventSubmitTelemetry = (commandInit = query) => { const eventData = (() => { - const commands = splitMonacoValuePerLines(commandInit) + const commands = without( + splitMonacoValuePerLines(commandInit) + .map((command) => removeMonacoComments(decode(command).trim())), + '' + ) const [commandLine, ...rest] = commands.map((command = '') => { const matchedCommand = REDIS_COMMANDS_ARRAY.find((commandName) => @@ -72,8 +77,7 @@ const QueryWrapper = (props: Props) => { }) const multiCommands = getMultiCommands(rest).replaceAll('\n', ';') - - const command = removeMonacoComments(decode([commandLine, multiCommands].join(';')).trim()) + const command = [commandLine, multiCommands].join('') ? [commandLine, multiCommands].join(';') : null return { command, @@ -85,10 +89,12 @@ const QueryWrapper = (props: Props) => { } })() - sendEventTelemetry({ - event: TelemetryEvent.WORKBENCH_COMMAND_SUBMITTED, - eventData - }) + if (eventData.command) { + sendEventTelemetry({ + event: TelemetryEvent.WORKBENCH_COMMAND_SUBMITTED, + eventData + }) + } } const handleSubmit = (value?: string) => { diff --git a/redisinsight/ui/src/constants/cliOutput.tsx b/redisinsight/ui/src/constants/cliOutput.tsx index 0867a678e6..cfbce3d247 100644 --- a/redisinsight/ui/src/constants/cliOutput.tsx +++ b/redisinsight/ui/src/constants/cliOutput.tsx @@ -1,5 +1,5 @@ import { EuiLink, EuiTextColor } from '@elastic/eui' -import React from 'react' +import React, { Fragment } from 'react' import { getRouterLinkProps } from 'uiSrc/services' export const ClearCommand = 'clear' @@ -17,7 +17,7 @@ export const InitOutputText = ( emptyOutput: boolean, onClick: () => void, ) => [ - <> + { emptyOutput && ( {'Try '} @@ -32,7 +32,7 @@ export const InitOutputText = ( , our advanced CLI. Check out our Quick Guides to learn more about Redis capabilities. )} - , + , '\n\n', 'Connecting...', '\n\n', diff --git a/redisinsight/ui/src/constants/keys.ts b/redisinsight/ui/src/constants/keys.ts index 51266504ba..c2f0ead827 100644 --- a/redisinsight/ui/src/constants/keys.ts +++ b/redisinsight/ui/src/constants/keys.ts @@ -26,7 +26,7 @@ export const GROUP_TYPES_DISPLAY = Object.freeze({ [KeyTypes.ReJSON]: 'JSON', [KeyTypes.JSON]: 'JSON', [KeyTypes.Stream]: 'Stream', - [ModulesKeyTypes.Graph]: 'GRAPH', + [ModulesKeyTypes.Graph]: 'Graph', [ModulesKeyTypes.TimeSeries]: 'TS', [CommandGroup.Bitmap]: 'Bitmap', [CommandGroup.Cluster]: 'Cluster', diff --git a/redisinsight/ui/src/pages/browser/components/browser-right-panel/BrowserRightPanel.tsx b/redisinsight/ui/src/pages/browser/components/browser-right-panel/BrowserRightPanel.tsx index be5b498528..e66fc57208 100644 --- a/redisinsight/ui/src/pages/browser/components/browser-right-panel/BrowserRightPanel.tsx +++ b/redisinsight/ui/src/pages/browser/components/browser-right-panel/BrowserRightPanel.tsx @@ -121,7 +121,7 @@ const BrowserRightPanel = (props: Props) => { keyProp={selectedKey} onCloseKey={closePanel} onEditKey={onEditKey} - onDeleteKey={onSelectKey} + onRemoveKey={onSelectKey} /> )} {isAddKeyPanelOpen && every([!isBulkActionsPanelOpen, !isCreateIndexPanelOpen], Boolean) && ( diff --git a/redisinsight/ui/src/pages/browser/components/filter-key-type/constants.ts b/redisinsight/ui/src/pages/browser/components/filter-key-type/constants.ts index 910df96e68..0b36eb707a 100644 --- a/redisinsight/ui/src/pages/browser/components/filter-key-type/constants.ts +++ b/redisinsight/ui/src/pages/browser/components/filter-key-type/constants.ts @@ -36,12 +36,12 @@ export const FILTER_KEY_TYPE_OPTIONS = [ color: GROUP_TYPES_COLORS[KeyTypes.ReJSON], }, { - text: 'STREAM', + text: 'Stream', value: KeyTypes.Stream, color: GROUP_TYPES_COLORS[KeyTypes.Stream], }, { - text: 'GRAPH', + text: 'Graph', value: ModulesKeyTypes.Graph, color: GROUP_TYPES_COLORS[ModulesKeyTypes.Graph], }, diff --git a/redisinsight/ui/src/pages/browser/components/hash-details/HashDetails.tsx b/redisinsight/ui/src/pages/browser/components/hash-details/HashDetails.tsx index cdc265c294..3f2294116b 100644 --- a/redisinsight/ui/src/pages/browser/components/hash-details/HashDetails.tsx +++ b/redisinsight/ui/src/pages/browser/components/hash-details/HashDetails.tsx @@ -74,10 +74,11 @@ interface IHashField extends HashFieldDto {} export interface Props { isFooterOpen: boolean + onRemoveKey: () => void } const HashDetails = (props: Props) => { - const { isFooterOpen } = props + const { isFooterOpen, onRemoveKey } = props const { total, @@ -135,7 +136,8 @@ const HashDetails = (props: Props) => { setDeleting(`${field + suffix}`) }, []) - const onSuccessRemoved = () => { + const onSuccessRemoved = (newTotalValue: number) => { + newTotalValue === 0 && onRemoveKey() sendEventTelemetry({ event: getBasedOnViewTypeEvent( viewType, diff --git a/redisinsight/ui/src/pages/browser/components/key-details-header/KeyDetailsHeader.tsx b/redisinsight/ui/src/pages/browser/components/key-details-header/KeyDetailsHeader.tsx index 5f184dc852..efec67a8cf 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details-header/KeyDetailsHeader.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-details-header/KeyDetailsHeader.tsx @@ -36,7 +36,7 @@ import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' import { getBasedOnViewTypeEvent, getRefreshEventData, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { formatBytes, - formatNameShort, + formatLongName, isEqualBuffers, isFormatEditable, MAX_TTL_NUMBER, @@ -117,7 +117,7 @@ const KeyDetailsHeader = ({ const keyNameRef = useRef(null) - const tooltipContent = formatNameShort(keyProp || '') + const tooltipContent = formatLongName(keyProp || '') const onMouseEnterKey = () => { setKeyIsHovering(true) diff --git a/redisinsight/ui/src/pages/browser/components/key-details/KeyDetails/KeyDetails.tsx b/redisinsight/ui/src/pages/browser/components/key-details/KeyDetails/KeyDetails.tsx index 2254a6d208..56576c48da 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details/KeyDetails/KeyDetails.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-details/KeyDetails/KeyDetails.tsx @@ -5,7 +5,7 @@ import { EuiButtonIcon, EuiToolTip } from '@elastic/eui' -import { isNull } from 'lodash' +import { curryRight, isNull } from 'lodash' import { useDispatch, useSelector } from 'react-redux' import cx from 'classnames' import { @@ -49,14 +49,15 @@ export interface Props { onToggleFullScreen: () => void onClose: (key: RedisResponseBuffer) => void onClosePanel: () => void - onRefresh: (key: RedisResponseBuffer, type: KeyTypes) => void + onRefresh: (key: RedisResponseBuffer, type: KeyTypes | ModulesKeyTypes) => void onDelete: (key: RedisResponseBuffer, type: string) => void + onRemoveKey: () => void onEditTTL: (key: RedisResponseBuffer, ttl: number) => void onEditKey: (key: RedisResponseBuffer, newKey: RedisResponseBuffer, onFailure?: () => void) => void } const KeyDetails = ({ ...props }: Props) => { - const { onClosePanel } = props + const { onClosePanel, onRemoveKey } = props const { loading, error = '', data } = useSelector(selectedKeySelector) const { type: selectedKeyType, name: selectedKey } = useSelector(selectedKeyDataSelector) ?? { type: KeyTypes.String, @@ -130,7 +131,7 @@ const KeyDetails = ({ ...props }: Props) => { setIsEdit={(isEdit) => setEditItem(isEdit)} /> ), - [KeyTypes.Hash]: , + [KeyTypes.Hash]: , [KeyTypes.List]: , [KeyTypes.ReJSON]: , [KeyTypes.Stream]: , diff --git a/redisinsight/ui/src/pages/browser/components/key-details/KeyDetailsWrapper.tsx b/redisinsight/ui/src/pages/browser/components/key-details/KeyDetailsWrapper.tsx index 4222e2ba29..36e7897506 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details/KeyDetailsWrapper.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-details/KeyDetailsWrapper.tsx @@ -31,7 +31,7 @@ export interface Props { onToggleFullScreen: () => void onCloseKey: () => void onEditKey: (key: RedisResponseBuffer, newKey: RedisResponseBuffer) => void - onDeleteKey: () => void + onRemoveKey: () => void keyProp: RedisResponseBuffer | null } @@ -42,7 +42,7 @@ const KeyDetailsWrapper = (props: Props) => { onToggleFullScreen, onCloseKey, onEditKey, - onDeleteKey, + onRemoveKey, keyProp } = props @@ -84,11 +84,11 @@ const KeyDetailsWrapper = (props: Props) => { if (type === KeyTypes.String) { dispatch(deleteKeyAction(key, () => { dispatch(resetStringValue()) - onDeleteKey() + onRemoveKey() })) return } - dispatch(deleteKeyAction(key, onDeleteKey)) + dispatch(deleteKeyAction(key, onRemoveKey)) } const handleRefreshKey = (key: RedisResponseBuffer, type: KeyTypes | ModulesKeyTypes) => { @@ -153,6 +153,7 @@ const KeyDetailsWrapper = (props: Props) => { onClosePanel={handleClosePanel} onRefresh={handleRefreshKey} onDelete={handleDeleteKey} + onRemoveKey={onRemoveKey} onEditTTL={handleEditTTL} onEditKey={handleEditKey} /> diff --git a/redisinsight/ui/src/pages/browser/styles.module.scss b/redisinsight/ui/src/pages/browser/styles.module.scss index c030ff3463..7d52a15f4f 100644 --- a/redisinsight/ui/src/pages/browser/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/styles.module.scss @@ -36,7 +36,7 @@ $breakpoint-to-hide-resize-panel: 1124px; .resizePanelLeft { min-width: 550px; :global(.euiResizablePanel__content) { - @media (min-width: 1124px) { + @media (min-width: $breakpoint-to-hide-resize-panel) { padding-right: 8px; } } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/top-namespace/TopNamespace.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/top-namespace/TopNamespace.spec.tsx index a6398b7fb5..9a8a0c1c14 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/top-namespace/TopNamespace.spec.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/top-namespace/TopNamespace.spec.tsx @@ -1,11 +1,23 @@ +import { cloneDeep } from 'lodash' import React from 'react' +import reactRouterDom from 'react-router-dom' import { instance, mock } from 'ts-mockito' -import { fireEvent, render, screen } from 'uiSrc/utils/test-utils' +import { resetBrowserTree } from 'uiSrc/slices/app/context' +import { changeKeyViewType } from 'uiSrc/slices/browser/keys' +import { KeyViewType } from 'uiSrc/slices/interfaces/keys' +import { act, cleanup, fireEvent, mockedStore, render, screen } from 'uiSrc/utils/test-utils' import TopNamespace, { Props } from './TopNamespace' const mockedProps = mock() +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + describe('TopNamespace', () => { it('should render', () => { expect(render()).toBeTruthy() @@ -125,4 +137,35 @@ describe('TopNamespace', () => { expect(queryByTestId('nsp-table-keys')).not.toBeInTheDocument() expect(queryByTestId('table-loader')).toBeInTheDocument() }) + + it('should render message when no namespaces', () => { + const mockedData = { + topKeysNsp: [], + topMemoryNsp: [] + } + render() + + expect(screen.queryByTestId('top-namespaces-empty')).toBeInTheDocument() + }) + + it('should call proper actions and push history after click tree view link', async () => { + const mockedData = { + topKeysNsp: [], + topMemoryNsp: [] + } + const pushMock = jest.fn() + reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock }) + + render() + + await act(() => { + fireEvent.click(screen.getByTestId('tree-view-page-link')) + }) + + const expectedActions = [resetBrowserTree(), changeKeyViewType(KeyViewType.Tree)] + + expect(store.getActions()).toEqual(expectedActions) + expect(pushMock).toHaveBeenCalledTimes(1) + expect(pushMock).toHaveBeenCalledWith('/instanceId/browser') + }) }) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/top-namespace/TopNamespace.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/top-namespace/TopNamespace.tsx index 8cc7a770a9..523bff42a7 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/top-namespace/TopNamespace.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/top-namespace/TopNamespace.tsx @@ -1,8 +1,14 @@ -import { EuiButton, EuiSwitch, EuiTitle } from '@elastic/eui' +import { EuiButton, EuiLink, EuiSwitch, EuiTitle } from '@elastic/eui' import cx from 'classnames' import React, { useEffect, useState } from 'react' +import { useDispatch } from 'react-redux' +import { useHistory, useParams } from 'react-router-dom' +import { Pages } from 'uiSrc/constants' import { DEFAULT_EXTRAPOLATION, SectionName, TableView } from 'uiSrc/pages/databaseAnalysis' import { TableLoader } from 'uiSrc/pages/databaseAnalysis/components' +import { resetBrowserTree } from 'uiSrc/slices/app/context' +import { changeKeyViewType } from 'uiSrc/slices/browser/keys' +import { KeyViewType } from 'uiSrc/slices/interfaces/keys' import { Nullable } from 'uiSrc/utils' import { DatabaseAnalysis } from 'apiSrc/modules/database-analysis/models' import Table from './Table' @@ -20,6 +26,10 @@ const TopNamespace = (props: Props) => { const [tableView, setTableView] = useState(TableView.MEMORY) const [isExtrapolated, setIsExtrapolated] = useState(true) + const { instanceId } = useParams<{ instanceId: string }>() + const history = useHistory() + const dispatch = useDispatch() + useEffect(() => { setIsExtrapolated(extrapolation !== DEFAULT_EXTRAPOLATION) }, [data, extrapolation]) @@ -28,8 +38,42 @@ const TopNamespace = (props: Props) => { return } + const handleTreeViewClick = (e: React.MouseEvent) => { + e.preventDefault() + + dispatch(resetBrowserTree()) + dispatch(changeKeyViewType(KeyViewType.Tree)) + history.push(Pages.browser(instanceId)) + } + if (!data?.topMemoryNsp?.length && !data?.topKeysNsp?.length) { - return null + return ( +
+
+ +

TOP NAMESPACES

+
+
+
+
+ + No namespaces to display + +

+ {'Configure the delimiter in '} + + Tree View + + {' to customize the namespaces displayed.'} +

+
+
+
+ ) } return ( diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/top-namespace/styles.module.scss b/redisinsight/ui/src/pages/databaseAnalysis/components/top-namespace/styles.module.scss index 651510eb1c..9775dd7699 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/top-namespace/styles.module.scss +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/top-namespace/styles.module.scss @@ -35,6 +35,19 @@ opacity: 1; } } + + .noNamespaceMsg { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + margin: 80px auto 100px; + + :global(.euiTitle) { + font-size: 18px; + margin-bottom: 10px; + } + } } .wrapper { diff --git a/redisinsight/ui/src/slices/browser/hash.ts b/redisinsight/ui/src/slices/browser/hash.ts index eb742db39b..9295762b1f 100644 --- a/redisinsight/ui/src/slices/browser/hash.ts +++ b/redisinsight/ui/src/slices/browser/hash.ts @@ -9,7 +9,6 @@ import successMessages from 'uiSrc/components/notifications/success-messages' import { GetHashFieldsResponse, AddFieldsToHashDto, - HashFieldDto, } from 'apiSrc/modules/browser/dto/hash.dto' import { deleteKeyFromList, @@ -312,7 +311,7 @@ export function fetchMoreHashFields( export function deleteHashFields( key: RedisResponseBuffer, fields: RedisResponseBuffer[], - onSuccessAction?: () => void, + onSuccessAction?: (newTotal?: number) => void, ) { return async (dispatch: AppDispatch, stateInit: () => RootState) => { dispatch(removeHashFields()) @@ -334,7 +333,7 @@ export function deleteHashFields( ) const newTotalValue = state.browser.hash.data.total - data.affected if (isStatusSuccessful(status)) { - onSuccessAction?.() + onSuccessAction?.(newTotalValue) dispatch(removeHashFieldsSuccess()) dispatch(removeFieldsFromList(fields)) if (newTotalValue > 0) { diff --git a/tests/e2e/pageObjects/browser-page.ts b/tests/e2e/pageObjects/browser-page.ts index 87c9f51202..433e909f74 100644 --- a/tests/e2e/pageObjects/browser-page.ts +++ b/tests/e2e/pageObjects/browser-page.ts @@ -103,6 +103,7 @@ export class BrowserPage { streamEntriesContainer = Selector('[data-testid=stream-entries-container]'); streamMessagesContainer = Selector('[data-testid=stream-messages-container]'); loader = Selector('[data-testid=type-loading]'); + newIndexPanel = Selector('[data-testid=create-index-panel]'); //LINKS internalLinkToWorkbench = Selector('[data-testid=internal-workbench-link]'); userSurveyLink = Selector('[data-testid=user-survey-link]'); @@ -127,6 +128,9 @@ export class BrowserPage { formatSwitcher = Selector('[data-testid=select-format-key-value]', { timeout: 2000 }); formatSwitcherIcon = Selector('img[data-testid^=key-value-formatter-option-selected]'); selectIndexDdn = Selector('[data-testid=select-index-placeholder],[data-testid=select-search-mode]', { timeout: 1000 }); + createIndexBtn = Selector('[data-testid=create-index-btn]'); + cancelIndexCreationBtn = Selector('[data-testid=create-index-cancel-btn]'); + confirmIndexCreationBtn = Selector('[data-testid=create-index-btn]'); //TABS streamTabGroups = Selector('[data-testid=stream-tab-Groups]'); streamTabConsumers = Selector('[data-testid=stream-tab-Consumers]'); @@ -170,6 +174,10 @@ export class BrowserPage { claimRetryCountInput = Selector('[data-testid=retry-count]'); lastIdInput = Selector('[data-testid=last-id-field]'); inlineItemEditor = Selector('[data-testid=inline-item-editor]'); + indexNameInput = Selector('[data-testid=index-name]'); + prefixFieldInput = Selector('[data-test-subj=comboBoxInput]'); + indexIdentifierInput = Selector('[data-testid^=identifier-]'); + indexFieldType = Selector('[data-testid^=field-type-]'); //TEXT ELEMENTS keySizeDetails = Selector('[data-testid=key-size-text]'); keyLengthDetails = Selector('[data-testid=key-length-text]'); diff --git a/tests/e2e/pageObjects/memory-efficiency-page.ts b/tests/e2e/pageObjects/memory-efficiency-page.ts index 604931de21..542235a88c 100644 --- a/tests/e2e/pageObjects/memory-efficiency-page.ts +++ b/tests/e2e/pageObjects/memory-efficiency-page.ts @@ -24,6 +24,8 @@ export class MemoryEfficiencyPage { scannedKeysInReport = Selector('[data-testid=bulk-delete-summary]'); topKeysTitle = Selector('[data-testid=top-keys-title]'); topKeysKeyName = Selector('[data-testid=top-keys-table-name]'); + topNamespacesEmptyContainer = Selector('[data-testid=top-namespaces-empty]'); + topNamespacesEmptyMessage = Selector('[data-testid=top-namespaces-message]'); // TABLE namespaceTable = Selector('[data-testid=nsp-table-memory]'); nameSpaceTableRows = this.namespaceTable.find('[data-testid^=row-]'); @@ -38,4 +40,6 @@ export class MemoryEfficiencyPage { thirdPoint = Selector('[data-testid*=bar-43200]'); fourthPoint = Selector('[data-testid*=bar-86400]'); noExpiryPoint = Selector('[data-testid*=bar-0-]:not(rect[data-testid=bar-0-0])'); + // LINKS + treeViewLink = Selector('[data-testid=tree-view-page-link]'); } diff --git a/tests/e2e/pageObjects/workbench-page.ts b/tests/e2e/pageObjects/workbench-page.ts index 515706870e..c0689a3e11 100644 --- a/tests/e2e/pageObjects/workbench-page.ts +++ b/tests/e2e/pageObjects/workbench-page.ts @@ -21,6 +21,7 @@ export class WorkbenchPage { cssQueryCardCommandResult = '[data-testid=query-common-result]'; cssCustomPluginTableResult = '[data-testid^=query-table-result-client]'; cssCommandExecutionDateTime = '[data-testid=command-execution-date-time]'; + cssRowInVirtualizedTable = '[data-testid^=row-]'; //------------------------------------------------------------------------------------------- //DECLARATION OF SELECTORS //*Declare all elements/components of the relevant page. @@ -115,6 +116,7 @@ export class WorkbenchPage { commandExecutionDateAndTime = Selector('[data-testid=command-execution-date-time]'); historyResultContainer = Selector('[data-testid=query-cli-card-result]'); historyResultRow = Selector('[data-testid=query-cli-card-result]'); + rowInVirtualizedTable = Selector('[data-testid^=row-]'); //MONACO ELEMENTS monacoCommandDetails = Selector('div.suggest-details-container'); monacoCloseCommandDetails = Selector('span.codicon-close'); diff --git a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts index 71223ceb97..011a157982 100644 --- a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts @@ -137,3 +137,51 @@ test await common.checkURL(externalPageLink); await t.switchToParentWindow(); }); +test + .before(async() => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + }) + .after(async() => { + await cliPage.sendCommandInCli(`FT.DROPINDEX ${indexName}`); + await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + })('Index creation', async t => { + const createIndexLink = 'https://redis.io/commands/ft.create/'; + + // Verify that user can cancel index creation + await t.click(browserPage.redisearchModeBtn); + await t.click(browserPage.selectIndexDdn); + await t.click(browserPage.createIndexBtn); + await t.expect(browserPage.newIndexPanel.exists).ok('New Index panel is not displayed'); + await t.click(browserPage.cancelIndexCreationBtn); + await t.expect(browserPage.newIndexPanel.exists).notOk('New Index panel is displayed'); + + // Verify that user can create an index with all mandatory parameters + await t.click(browserPage.redisearchModeBtn); + await t.click(browserPage.selectIndexDdn); + await t.click(browserPage.createIndexBtn); + await t.expect(browserPage.newIndexPanel.exists).ok('New Index panel is not displayed'); + // Verify that user can see a link to create a profound index and navigate + await t.click(browserPage.newIndexPanel.find('a')); + await common.checkURL(createIndexLink); + await t.switchToParentWindow(); + + // Verify that user can create an index with multiple prefixes + await t.click(browserPage.indexNameInput); + await t.typeText(browserPage.indexNameInput, indexName); + await t.click(browserPage.prefixFieldInput); + await t.typeText(browserPage.prefixFieldInput, 'device:'); + await t.pressKey('enter'); + await t.typeText(browserPage.prefixFieldInput, 'mobile_'); + await t.pressKey('enter'); + await t.typeText(browserPage.prefixFieldInput, 'user_'); + await t.pressKey('enter'); + await t.expect(browserPage.prefixFieldInput.find('button').count).eql(3, '3 prefixes are not displayed'); + + // Verify that user can create an index with multiple fields (up to 20) + await t.click(browserPage.indexIdentifierInput); + await t.typeText(browserPage.indexIdentifierInput, 'k0'); + await t.click(browserPage.confirmIndexCreationBtn); + await t.expect(browserPage.newIndexPanel.exists).notOk('New Index panel is displayed'); + await t.click(browserPage.selectIndexDdn); + await browserPage.selectIndexByName(indexName); + }); diff --git a/tests/e2e/tests/critical-path/memory-efficiency/memory-efficiency.e2e.ts b/tests/e2e/tests/critical-path/memory-efficiency/memory-efficiency.e2e.ts index 7d17cf69f7..0a97f24c49 100644 --- a/tests/e2e/tests/critical-path/memory-efficiency/memory-efficiency.e2e.ts +++ b/tests/e2e/tests/critical-path/memory-efficiency/memory-efficiency.e2e.ts @@ -5,7 +5,6 @@ import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; -import { Selector } from 'testcafe'; const memoryEfficiencyPage = new MemoryEfficiencyPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); @@ -62,7 +61,7 @@ test await browserPage.addStreamKey(streamKeyNameDelimiter, 'field', 'value', keysTTL[2]); if (await browserPage.submitTooltipBtn.exists) { await t.click(browserPage.submitTooltipBtn); - }; + } await cliPage.addKeysFromCliWithDelimiter('MSET', 15); await t.click(browserPage.treeViewButton); // Go to Analysis Tools page @@ -77,6 +76,8 @@ test await browserPage.deleteKeyByName(streamKeyNameDelimiter); await deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Keyspaces displaying in Summary per keyspaces table', async t => { + const noNamespacesMessage = 'No namespaces to displayConfigure the delimiter in Tree View to customize the namespaces displayed.'; + // Create new report await t.click(memoryEfficiencyPage.newReportBtn); // Verify that up to 15 keyspaces based on the delimiter set in the Tree view are displayed on memory efficiency page @@ -100,8 +101,9 @@ test await t.expect(await browserPage.isKeyIsDisplayedInTheList(streamKeyName)).ok('Key is not found'); // Clear filter - await t.click(browserPage.treeViewButton); - await t.click(browserPage.clearFilterButton); + await t + .click(browserPage.treeViewButton) + .click(browserPage.clearFilterButton); // Change delimiter await browserPage.changeDelimiterInTreeView('-'); // Go to Analysis Tools page @@ -111,6 +113,21 @@ test // Verify that delimiter can be changed in Tree View and applied await t.expect(memoryEfficiencyPage.nameSpaceTableRows.count).eql(1, 'New delimiter not applied'); await t.expect(memoryEfficiencyPage.nameSpaceTableRows.nth(0).textContent).contains(keySpaces[5], 'Keyspace not displayed'); + + // No namespaces message with link + await t.click(myRedisDatabasePage.browserButton); + // Change delimiter to delimiter with no keys + await browserPage.changeDelimiterInTreeView('+'); + // Go to Analysis Tools page and create report + await t + .click(myRedisDatabasePage.analysisPageButton) + .click(memoryEfficiencyPage.newReportBtn); + // Verify that user can see the message when he do not have any namespaces selected in delimiter + await t.expect(memoryEfficiencyPage.topNamespacesEmptyContainer.exists).ok('No namespaces section not displayed'); + await t.expect(memoryEfficiencyPage.topNamespacesEmptyMessage.textContent).contains(noNamespacesMessage, 'No namespaces message not displayed/correct'); + // Verify that user can redirect to Tree view by clicking on button + await t.click(memoryEfficiencyPage.treeViewLink); + await t.expect(browserPage.treeViewArea.exists).ok('Tree view not opened'); }); test .before(async t => { @@ -200,7 +217,7 @@ test await browserPage.deleteKeyByName(streamKeyNameDelimiter); await deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Summary per expiration time', async t => { - const yAxis: number = 218; + const yAxis = 218; // Create new report await t.click(memoryEfficiencyPage.newReportBtn); // Points are displayed in graph according to their TTL diff --git a/tests/e2e/tests/regression/workbench/command-results.e2e.ts b/tests/e2e/tests/regression/workbench/command-results.e2e.ts index 08662a5a68..a5dc8bd378 100644 --- a/tests/e2e/tests/regression/workbench/command-results.e2e.ts +++ b/tests/e2e/tests/regression/workbench/command-results.e2e.ts @@ -90,3 +90,30 @@ test.skip // Verify that search results are displayed in Text view await t.expect(workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssQueryTextResult).visible).ok('The result is displayed in Text view'); }); +test('Big output in workbench is visible in virtuallized table', async t => { + // Send commands + const command = 'graph.query t "UNWIND range(1,1000) AS x return x"'; + const bottomText = 'Query internal execution time'; + let numberOfScrolls = 0; + + // Send command in workbench with Text view type + await workbenchPage.sendCommandInWorkbench(command); + await workbenchPage.selectViewTypeText(); + + const containerOfCommand = await workbenchPage.getCardContainerByCommand(command); + const listItems = containerOfCommand.find(workbenchPage.cssRowInVirtualizedTable); + const lastExpectedItem = listItems.withText(bottomText); + + // Scroll down the virtualized list until the last row + while (!await lastExpectedItem.exists && numberOfScrolls < 100) { + const currentLastRenderedItemIndex = await listItems.count - 1; + const currentLastRenderedItemText = await listItems.nth(currentLastRenderedItemIndex).textContent; + const currentLastRenderedItem = listItems.withText(currentLastRenderedItemText); + + await t.scrollIntoView(currentLastRenderedItem); + numberOfScrolls++; + } + + // Verify that all commands scrolled + await t.expect(lastExpectedItem.visible).ok('Final execution time message not displayed'); +});