From 941e021bae0685bcdc1f1c7f4f12df2e6e7dfa5a Mon Sep 17 00:00:00 2001 From: zalenskiSofteq Date: Fri, 20 Oct 2023 10:19:43 +0200 Subject: [PATCH 01/12] #RI-4828 - Re-work the Tree view --- patches/react-vtree+3.0.0-beta.3.patch | 24 ++ .../src/assets/img/browser/treeViewSort.svg | 10 + .../components/keys-summary/KeysSummary.tsx | 11 +- .../keys-summary/styles.module.scss | 4 + .../virtual-tree/VirtualTree.spec.tsx | 25 -- .../components/virtual-tree/VirtualTree.tsx | 231 +++++++------ .../components/Node/Node.spec.tsx | 12 +- .../virtual-tree/components/Node/Node.tsx | 323 +++++++++++++++--- .../components/Node/styles.module.scss | 87 ++++- .../src/components/virtual-tree/interfaces.ts | 22 +- .../virtual-tree/styles.module.scss | 4 + redisinsight/ui/src/constants/browser.ts | 3 +- .../ui/src/helpers/constructKeysToTree.ts | 76 +++-- .../tests/constructKeysToTreeMockResult.ts | 203 +++++------ .../browser-left-panel/BrowserLeftPanel.tsx | 2 + .../browser-right-panel/BrowserRightPanel.tsx | 8 +- .../filter-key-type/FilterKeyType.tsx | 2 + .../key-details-header/KeyDetailsHeader.tsx | 4 +- .../browser/components/key-list/KeyList.tsx | 47 +-- .../components/key-tree/KeyTree.spec.tsx | 58 +--- .../browser/components/key-tree/KeyTree.tsx | 229 ++++++------- .../KeyTreeDelimiter.spec.tsx | 106 ------ .../KeyTreeDelimiter/KeyTreeDelimiter.tsx | 87 ----- .../key-tree/KeyTreeDelimiter/index.ts | 3 - .../KeyTreeDelimiter/styles.module.scss | 68 ---- .../KeyTreeSettings/KeyTreeSettings.spec.tsx | 154 +++++++++ .../KeyTreeSettings/KeyTreeSettings.tsx | 174 ++++++++++ .../key-tree/KeyTreeSettings/index.ts | 3 + .../KeyTreeSettings/styles.module.scss | 84 +++++ .../browser/components/key-tree/index.ts | 5 + .../components/key-tree/styles.module.scss | 62 +--- .../components/keys-header/KeysHeader.tsx | 1 + .../no-keys-found/styles.module.scss | 2 +- .../no-keys-message/NoKeysMessage.spec.tsx | 21 ++ .../no-keys-message/NoKeysMessage.tsx | 65 ++++ .../components/no-keys-message/index.ts | 3 + .../search-key-list/SearchKeyList.tsx | 3 + redisinsight/ui/src/slices/app/context.ts | 48 +-- redisinsight/ui/src/slices/browser/keys.ts | 33 ++ redisinsight/ui/src/slices/interfaces/app.ts | 13 +- .../ui/src/slices/tests/app/context.spec.ts | 196 +---------- redisinsight/ui/src/telemetry/events.ts | 1 + redisinsight/ui/src/utils/tests/tree.spec.ts | 23 +- redisinsight/ui/src/utils/tree.ts | 25 -- 44 files changed, 1395 insertions(+), 1170 deletions(-) create mode 100644 patches/react-vtree+3.0.0-beta.3.patch create mode 100644 redisinsight/ui/src/assets/img/browser/treeViewSort.svg delete mode 100644 redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.spec.tsx delete mode 100644 redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.tsx delete mode 100644 redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/index.ts delete mode 100644 redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/styles.module.scss create mode 100644 redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.spec.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/index.ts create mode 100644 redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/styles.module.scss create mode 100644 redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.spec.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/no-keys-message/index.ts diff --git a/patches/react-vtree+3.0.0-beta.3.patch b/patches/react-vtree+3.0.0-beta.3.patch new file mode 100644 index 0000000000..7629fc04cc --- /dev/null +++ b/patches/react-vtree+3.0.0-beta.3.patch @@ -0,0 +1,24 @@ +diff --git a/node_modules/react-vtree/dist/es/Tree.d.ts b/node_modules/react-vtree/dist/es/Tree.d.ts +index 5e7f57e..15c09d4 100644 +--- a/node_modules/react-vtree/dist/es/Tree.d.ts ++++ b/node_modules/react-vtree/dist/es/Tree.d.ts +@@ -24,6 +24,7 @@ export declare type NodePublicState = Readonly<{ + data: TData; + setOpen: (state: boolean) => Promise; + }> & { ++ index: number; + isOpen: boolean; + }; + export declare type NodeRecord> = Readonly<{ +diff --git a/node_modules/react-vtree/dist/es/Tree.js b/node_modules/react-vtree/dist/es/Tree.js +index 2b1c7c0..b22e873 100644 +--- a/node_modules/react-vtree/dist/es/Tree.js ++++ b/node_modules/react-vtree/dist/es/Tree.js +@@ -19,6 +19,7 @@ export var Row = function Row(_ref) { + return /*#__PURE__*/React.createElement(Node, Object.assign({ + isScrolling: isScrolling, + style: style, ++ index: index, + treeData: treeData + }, data)); + }; diff --git a/redisinsight/ui/src/assets/img/browser/treeViewSort.svg b/redisinsight/ui/src/assets/img/browser/treeViewSort.svg new file mode 100644 index 0000000000..97b88a16ed --- /dev/null +++ b/redisinsight/ui/src/assets/img/browser/treeViewSort.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/redisinsight/ui/src/components/keys-summary/KeysSummary.tsx b/redisinsight/ui/src/components/keys-summary/KeysSummary.tsx index aea5e73c2d..979c19b881 100644 --- a/redisinsight/ui/src/components/keys-summary/KeysSummary.tsx +++ b/redisinsight/ui/src/components/keys-summary/KeysSummary.tsx @@ -2,10 +2,14 @@ import React from 'react' import cx from 'classnames' import { isNull } from 'lodash' import { EuiText, EuiTextColor } from '@elastic/eui' +import { useSelector } from 'react-redux' import { numberWithSpaces, nullableNumberWithSpaces } from 'uiSrc/utils/numbers' -import ScanMore from '../scan-more' +import { KeyViewType } from 'uiSrc/slices/interfaces/keys' +import { keysSelector } from 'uiSrc/slices/browser/keys' +import { KeyTreeSettings } from 'uiSrc/pages/browser/components/key-tree' +import ScanMore from '../scan-more' import styles from './styles.module.scss' export interface Props { @@ -41,6 +45,8 @@ const KeysSummary = (props: Props) => { && nextCursor !== '0' ? '~' : '' + const { viewType } = useSelector(keysSelector) + return ( <> {(!!totalItemsCount || isNull(totalItemsCount)) && ( @@ -88,6 +94,9 @@ const KeysSummary = (props: Props) => { )} + {viewType === KeyViewType.Tree && ( + + )} )} {loading && !totalItemsCount && !isNull(totalItemsCount) && ( diff --git a/redisinsight/ui/src/components/keys-summary/styles.module.scss b/redisinsight/ui/src/components/keys-summary/styles.module.scss index 198f838ff6..294a787183 100644 --- a/redisinsight/ui/src/components/keys-summary/styles.module.scss +++ b/redisinsight/ui/src/components/keys-summary/styles.module.scss @@ -1,3 +1,7 @@ +.content { + display: flex; +} + .loading { opacity: 0; } diff --git a/redisinsight/ui/src/components/virtual-tree/VirtualTree.spec.tsx b/redisinsight/ui/src/components/virtual-tree/VirtualTree.spec.tsx index fcd74e9a79..b869123dd7 100644 --- a/redisinsight/ui/src/components/virtual-tree/VirtualTree.spec.tsx +++ b/redisinsight/ui/src/components/virtual-tree/VirtualTree.spec.tsx @@ -15,10 +15,6 @@ const mockedItems = [ }, ] -export const mockLeafKeys = { - test: { name: 'test', type: 'hash', ttl: -1, size: 9849176 } -} - export const mockVirtualTreeResult = [{ children: [{ children: [], @@ -41,7 +37,6 @@ export const mockVirtualTreeResult = [{ keyApproximate: 0.01, keyCount: 1, name: 'test', - keys: mockLeafKeys }] jest.mock('uiSrc/services', () => ({ @@ -79,24 +74,4 @@ describe('VirtualTree', () => { expect(queryByTestId('node-item_test')).toBeInTheDocument() }) - - it('should select first leaf "Keys" by default', async () => { - const mockConstructingTreeFn = jest.fn() - const mockOnStatusSelected = jest.fn() - const mockOnSelectLeaf = jest.fn() - - render( - - ) - - expect(mockOnSelectLeaf).toHaveBeenCalledWith(mockLeafKeys) - expect(mockOnStatusSelected).toHaveBeenCalledWith('test', mockLeafKeys) - }) }) diff --git a/redisinsight/ui/src/components/virtual-tree/VirtualTree.tsx b/redisinsight/ui/src/components/virtual-tree/VirtualTree.tsx index e9f2ddbab1..ce2dbcee55 100644 --- a/redisinsight/ui/src/components/virtual-tree/VirtualTree.tsx +++ b/redisinsight/ui/src/components/virtual-tree/VirtualTree.tsx @@ -1,22 +1,24 @@ -import React, { useCallback, useContext, useEffect, useState } from 'react' +import React, { useCallback, useContext, useEffect, useRef, useState } from 'react' import AutoSizer from 'react-virtualized-auto-sizer' -import { isArray, isEmpty } from 'lodash' +import { debounce, get, set } from 'lodash' import { TreeWalker, TreeWalkerValue, FixedSizeTree as Tree, } from 'react-vtree' -import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui' +import { EuiIcon, EuiLoadingSpinner, EuiProgress } from '@elastic/eui' import { useDispatch } from 'react-redux' -import { findTreeNode, getTreeLeafField, Maybe } from 'uiSrc/utils' +import { bufferToString, Maybe, Nullable } from 'uiSrc/utils' import { useDisposableWebworker } from 'uiSrc/services' import { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys' import { ThemeContext } from 'uiSrc/contexts/themeContext' -import { DEFAULT_DELIMITER, Theme } from 'uiSrc/constants' +import { DEFAULT_DELIMITER, DEFAULT_TREE_SORTING, KeyTypes, ModulesKeyTypes, SortOrder, Theme } from 'uiSrc/constants' import KeyLightSVG from 'uiSrc/assets/img/sidebar/browser.svg' import KeyDarkSVG from 'uiSrc/assets/img/sidebar/browser_active.svg' -import { resetBrowserTree } from 'uiSrc/slices/app/context' +import { RedisResponseBuffer, RedisString } from 'uiSrc/slices/interfaces' +import { fetchKeysMetadataTree } from 'uiSrc/slices/browser/keys' +import { GetKeyInfoResponse } from 'apiSrc/modules/browser/dto' import { Node } from './components/Node' import { NodeMeta, TreeData, TreeNode } from './interfaces' @@ -28,21 +30,21 @@ export interface Props { delimiter?: string loadingIcon?: string loading: boolean - selectDefaultLeaf?: boolean - statusSelected: { - [key: string]: { - [key: string]: IKeyPropTypes - } - }, - statusOpen: { - [key: string]: boolean - } + deleting: boolean + sorting: Maybe + commonFilterType: Nullable + statusSelected: Nullable, + statusOpen: OpenedNodes webworkerFn: (...args: any) => any - onSelectLeaf?: (items: any[]) => void - disableSelectDefaultLeaf?: () => void onStatusOpen?: (name: string, value: boolean) => void - onStatusSelected?: (id: string, keys: any) => void + onStatusSelected?: (key: RedisString) => void setConstructingTree: (status: boolean) => void + onDeleteLeaf: (key: RedisResponseBuffer) => void + onDeleteClicked: (type: KeyTypes | ModulesKeyTypes) => void +} + +interface OpenedNodes { + [key: string]: boolean; } export const KEYS = 'keys' @@ -53,26 +55,34 @@ const VirtualTree = (props: Props) => { delimiter = DEFAULT_DELIMITER, loadingIcon = 'empty', statusOpen = {}, - statusSelected = {}, + statusSelected, loading, - selectDefaultLeaf, + deleting, + sorting = DEFAULT_TREE_SORTING, + commonFilterType, onStatusOpen, onStatusSelected, - onSelectLeaf, setConstructingTree, - disableSelectDefaultLeaf, - webworkerFn = () => {} + webworkerFn = () => {}, + onDeleteClicked, + onDeleteLeaf, } = props const { theme } = useContext(ThemeContext) - const [nodes, setNodes] = useState([]) + const [rerenderState, rerender] = useState({}) + const controller = useRef>(null) + const elements = useRef({}) + const nodes = useRef([]) + const { result, run: runWebworker } = useDisposableWebworker(webworkerFn) const dispatch = useDispatch() useEffect(() => - () => setNodes([]), - []) + () => { + nodes.current = [] + elements.current = [] + }, []) // receive result from the "runWebworker" useEffect(() => { @@ -80,73 +90,82 @@ const VirtualTree = (props: Props) => { return } - setNodes(result) + elements.current = [] + nodes.current = result + rerender({}) setConstructingTree?.(false) }, [result]) - // select "root" Keys after render a new tree (construct a tree) useEffect(() => { - if (nodes.length === 0 || !selectDefaultLeaf || loading) { + if (!items?.length) { + nodes.current = [] + elements.current = [] + rerender({}) + runWebworker?.({ items: [], delimiter, sorting }) return } - if (isArray(nodes) && isEmpty(statusSelected)) { - let selectedLeaf: Maybe = nodes?.find(({ children = [] }) => children.length === 0) - - // if Keys folder not exists - first folder should be opened - if (!selectedLeaf && nodes.length) { - selectedLeaf = nodes?.[0] - - onStatusOpen?.(selectedLeaf?.fullName ?? '', true) - onStatusSelected?.( - `${selectedLeaf?.fullName + KEYS + delimiter + KEYS + delimiter}` ?? '', - selectedLeaf?.keys ?? selectedLeaf?.children?.[0]?.keys - ) - } else { - // if Keys folder exist - open it - onStatusSelected?.(selectedLeaf?.fullName ?? '', selectedLeaf?.keys) - } + setConstructingTree(true) + runWebworker?.({ items, delimiter, sorting }) + }, [items, delimiter]) - disableSelectDefaultLeaf?.() - onSelectLeaf?.(selectedLeaf?.keys ?? selectedLeaf?.children?.[0]?.keys ?? []) - } - }, [nodes, loading, selectDefaultLeaf]) + const handleUpdateSelected = useCallback((name: RedisString) => { + onStatusSelected?.(name) + }, [onStatusSelected]) - useEffect(() => { - if (isEmpty(statusSelected) || !nodes.length) { - return - } + const handleUpdateOpen = useCallback((fullName: string, value: boolean) => { + onStatusOpen?.(fullName, value) + }, [onStatusOpen, nodes]) - // if selected Keys folder is not exists (after a new search) needs reset Browser state - const selectedLeafExists = !!findTreeNode(nodes, Object.keys(statusSelected)?.[0], 'fullName') + const updateNodeByPath = (path: string, data: any) => { + const paths = path.replaceAll('.', '.children.') - if (!selectedLeafExists) { - dispatch(resetBrowserTree()) - } - }, [nodes]) + const node = get(nodes.current, paths) + const fullData = { ...node, ...data } - useEffect(() => { - if (!items?.length) { - setNodes([]) - runWebworker?.({ items: [], delimiter }) - return + if (node) { + set(nodes.current, paths, fullData) } + } - setConstructingTree(true) - runWebworker?.({ items, delimiter }) - }, [items, delimiter]) + const formatItem = useCallback((item: GetKeyInfoResponse) => ({ + ...item, + nameString: bufferToString(item.name as string) + }), []) - const handleSelectLeaf = useCallback((keys: any[]) => { - onSelectLeaf?.(keys) - }, [onSelectLeaf]) + const getMetadata = useCallback(( + itemsInit: any[] = [] + ): void => { + dispatch(fetchKeysMetadataTree( + itemsInit, + commonFilterType, + controller.current?.signal, + (loadedItems) => + onSuccessFetchedMetadata(loadedItems), + () => { rerender({}) } + )) + }, [commonFilterType]) - const handleUpdateSelected = useCallback((fullName: string, keys: any) => { - onStatusSelected?.(fullName, keys) - }, [onStatusSelected]) + const onSuccessFetchedMetadata = ( + loadedItems: any[], + ) => { + const items = loadedItems.map(formatItem) + + items.forEach((item) => updateNodeByPath(item.path, item)) + + rerender({}) + } - const handleUpdateOpen = useCallback((name: string, value: boolean) => { - onStatusOpen?.(name, value) - }, [onStatusOpen]) + const getMetadataDebounced = debounce(() => { + const entries = Object.entries(elements.current) + + getMetadata(entries) + }, 100) + + const getMetadataNode = (nameBuffer: any, path: string) => { + elements.current[path] = nameBuffer + getMetadataDebounced() + } // This helper function constructs the object that will be sent back at the step // [2] during the treeWalker function work. Except for the mandatory `data` @@ -157,18 +176,27 @@ const VirtualTree = (props: Props) => { ): TreeWalkerValue => ({ data: { id: node.id.toString(), - isLeaf: node.children?.length === 0, + isLeaf: node.isLeaf, keyCount: node.keyCount, name: node.name, + nameString: node.nameString, + nameBuffer: node.nameBuffer, + ttl: node.ttl, + size: node.size, + type: node.type, fullName: node.fullName, + shortName: node.shortName, nestingLevel, - setItems: handleSelectLeaf, + deleting, + path: node.path, + getMetadata: getMetadataNode, + onDeleteClicked, updateStatusSelected: handleUpdateSelected, updateStatusOpen: handleUpdateOpen, + onDelete: onDeleteLeaf, leafIcon: theme === Theme.Dark ? KeyDarkSVG : KeyLightSVG, keyApproximate: node.keyApproximate, - keys: node.keys || node?.[getTreeLeafField(delimiter)], - isSelected: Object.keys(statusSelected)[0] === node.fullName, + isSelected: !!node.isLeaf && statusSelected === node?.nameString, isOpenByDefault: statusOpen[node.fullName], }, nestingLevel, @@ -180,8 +208,8 @@ const VirtualTree = (props: Props) => { const treeWalker = useCallback( function* treeWalker(): ReturnType> { // Step [1]: Define the root multiple nodes of our tree - for (let i = 0; i < nodes.length; i++) { - yield getNodeData(nodes[i], 0) + for (let i = 0; i < nodes.current.length; i++) { + yield getNodeData(nodes.current[i], 0) } // Step [2]: Get the parent component back. It will be the object @@ -199,25 +227,38 @@ const VirtualTree = (props: Props) => { } } }, - [nodes, statusSelected], + [statusSelected, statusOpen, rerenderState], ) return ( {({ height, width }) => ( -
- { nodes.length > 0 && ( - - {Node} - +
+ { nodes.current.length > 0 && ( + <> + {loading && ( + + )} + + {Node} + + )} - { nodes.length === 0 && loading && ( + { nodes.current.length === 0 && loading && (
diff --git a/redisinsight/ui/src/components/virtual-tree/components/Node/Node.spec.tsx b/redisinsight/ui/src/components/virtual-tree/components/Node/Node.spec.tsx index 2b39ac5f23..427ecc8f77 100644 --- a/redisinsight/ui/src/components/virtual-tree/components/Node/Node.spec.tsx +++ b/redisinsight/ui/src/components/virtual-tree/components/Node/Node.spec.tsx @@ -5,7 +5,7 @@ import { render, screen } from 'uiSrc/utils/test-utils' import KeyDarkSVG from 'uiSrc/assets/img/sidebar/browser_active.svg' import Node from './Node' import { TreeData } from '../../interfaces' -import { mockLeafKeys, mockVirtualTreeResult } from '../../VirtualTree.spec' +import { mockVirtualTreeResult } from '../../VirtualTree.spec' const mockedProps = mock>() const mockedPropsData = mock() @@ -52,7 +52,6 @@ describe('Node', () => { }) it('"setItems", "updateStatusSelected" should be called after click on Leaf', () => { - const mockSetItems = jest.fn() const mockUpdateStatusSelected = jest.fn() const mockUpdateStatusOpen = jest.fn() const mockSetOpen = jest.fn() @@ -61,8 +60,6 @@ describe('Node', () => { ...mockedData, isLeaf: true, fullName: mockDataFullName, - keys: mockLeafKeys, - setItems: mockSetItems, updateStatusSelected: mockUpdateStatusSelected, updateStatusOpen: mockUpdateStatusOpen, } @@ -76,14 +73,12 @@ describe('Node', () => { screen.getByTestId(`node-item_${mockDataFullName}`).click() - expect(mockSetItems).toBeCalledWith(mockLeafKeys) - expect(mockUpdateStatusSelected).toBeCalledWith(mockDataFullName, mockLeafKeys) + expect(mockUpdateStatusSelected).toBeCalledWith(mockDataFullName) expect(mockUpdateStatusOpen).toBeCalledWith(mockDataFullName, true) expect(mockSetOpen).not.toBeCalled() }) it('"updateStatusOpen", "setOpen" should be called after click on Node', () => { - const mockSetItems = jest.fn() const mockUpdateStatusSelected = jest.fn() const mockUpdateStatusOpen = jest.fn() const mockSetOpen = jest.fn() @@ -93,8 +88,6 @@ describe('Node', () => { ...mockedData, isLeaf: mockIsOpen, fullName: mockDataFullName, - keys: mockLeafKeys, - setItems: mockSetItems, updateStatusSelected: mockUpdateStatusSelected, updateStatusOpen: mockUpdateStatusOpen, } @@ -108,7 +101,6 @@ describe('Node', () => { screen.getByTestId(`node-item_${mockDataFullName}`).click() - expect(mockSetItems).not.toBeCalled() expect(mockUpdateStatusSelected).not.toBeCalled() expect(mockUpdateStatusOpen).toHaveBeenCalledWith(mockDataFullName, !mockIsOpen) expect(mockSetOpen).toBeCalledWith(!mockIsOpen) diff --git a/redisinsight/ui/src/components/virtual-tree/components/Node/Node.tsx b/redisinsight/ui/src/components/virtual-tree/components/Node/Node.tsx index 11850e3968..595e4af36c 100644 --- a/redisinsight/ui/src/components/virtual-tree/components/Node/Node.tsx +++ b/redisinsight/ui/src/components/virtual-tree/components/Node/Node.tsx @@ -1,8 +1,31 @@ -import React, { useEffect } from 'react' +import React, { useEffect, useState } from 'react' import { NodePublicState } from 'react-vtree/dist/es/Tree' import cx from 'classnames' -import { EuiIcon, EuiToolTip, keys as ElasticKeys } from '@elastic/eui' +import { isUndefined } from 'lodash' +import { + EuiIcon, + EuiToolTip, + keys as ElasticKeys, + EuiButtonIcon, + EuiLoadingContent, + EuiText, + EuiTextColor, + EuiPopover, + EuiSpacer, + EuiButton, +} from '@elastic/eui' +import GroupBadge from 'uiSrc/components/group-badge' +import { + Maybe, + formatBytes, + formatLongName, + replaceSpaces, + truncateNumberToDuration, + truncateNumberToFirstUnit, + truncateTTLToSeconds, +} from 'uiSrc/utils' +import { KeyTypes, ModulesKeyTypes } from 'uiSrc/constants' import { TreeData } from '../../interfaces' import styles from './styles.module.scss' @@ -12,38 +35,53 @@ import styles from './styles.module.scss' const Node = ({ data, isOpen, + index, style, - setOpen + setOpen, }: NodePublicState) => { const { + id: nodeId, isLeaf, - leafIcon, - keys, - name, keyCount, nestingLevel, fullName, + nameBuffer, + path, + type, + ttl, + size, + deleting, + shortName, + nameString, keyApproximate, isSelected, - setItems, + getMetadata, + onDelete, + onDeleteClicked, updateStatusOpen, updateStatusSelected, } = data + const [deletePopoverId, setDeletePopoverId] = useState>(undefined) + + useEffect(() => { + if (!size && isLeaf && nameBuffer) { + getMetadata(nameBuffer, path) + } + }, []) + useEffect(() => { - if (isSelected && keys) { - updateStatusSelected?.(fullName, keys) + if (isSelected && nameBuffer) { + updateStatusSelected?.(nameBuffer) } - }, [keys, isSelected]) + }, [isSelected]) const handleClick = () => { - if (isLeaf && keys && !isSelected) { - setItems?.(keys) - updateStatusSelected?.(fullName, keys) + if (isLeaf && !isSelected) { + updateStatusSelected?.(nameBuffer) } updateStatusOpen?.(fullName, !isOpen) - !isLeaf && setOpen(!isOpen) } @@ -53,6 +91,216 @@ const Node = ({ } } + const handleDelete = () => { + onDelete(nameBuffer) + setDeletePopoverId(undefined) + } + + const handleDeletePopoverOpen = (index: Maybe, type: KeyTypes | ModulesKeyTypes) => { + if (index !== deletePopoverId) { + onDeleteClicked(type) + } + setDeletePopoverId(index !== deletePopoverId ? index : undefined) + } + + const Folder = () => ( + <> +
+ + + {nameString} +
+
+
+ {keyApproximate ? `${keyApproximate < 1 ? '<1' : Math.round(keyApproximate)}%` : '' } +
+
{keyCount ?? ''}
+
+ + ) + + const Leaf = () => ( + <> + + + + + + ) + + const LeafType = () => ( + <> + {!type && } + {!!type &&
} + + ) + + const LeafName = () => { + // Better to cut the long string, because it could affect virtual scroll performance + const nameContent = replaceSpaces(shortName?.substring?.(0, 200)) + const nameTooltipContent = formatLongName(nameString) + + return ( +
+ +
+ + <>{nameContent} + +
+
+
+ ) + } + + const LeafTTL = () => { + if (isUndefined(ttl)) { + return + } + if (ttl === -1) { + return ( + + No limit + + ) + } + return ( + +
+ + {`${truncateTTLToSeconds(ttl)} s`} +
+ {`(${truncateNumberToDuration(ttl)})`} + + )} + > + <>{truncateNumberToFirstUnit(ttl)} +
+
+
+ ) + } + + const LeafSize = () => { + if (isUndefined(size)) { + return + } + + if (!size) { + return ( + + - + + ) + } + return ( + <> + +
+ + {formatBytes(size, 3)} + + )} + > + <>{formatBytes(size, 0)} + +
+
+ setDeletePopoverId(undefined)} + panelPaddingSize="l" + panelClassName={styles.deletePopover} + button={( + handleDeletePopoverOpen(nodeId, type)} + aria-label="Delete Key" + data-testid={`delete-key-btn-${nameString}`} + /> + )} + onClick={(e) => e.stopPropagation()} + > + <> + +

{formatLongName(nameString)}

+ will be deleted. +
+ + + Delete + + +
+ + ) + } + const Node = (
{}} data-testid={`node-item_${fullName}`} > -
- {!isLeaf && ( - <> - - - {name} - - )} - - {isLeaf && ( - <> - - Keys - - )} -
-
- {keyCount ?? ''} - - {keyApproximate ? `${keyApproximate < 1 ? '<1' : Math.round(keyApproximate)}%` : '' } - -
+ {!isLeaf && } + {isLeaf && }
) - const content = ( + const tooltipContent = ( <> {`${fullName}*`}
@@ -116,12 +332,17 @@ const Node = ({ ...style, paddingLeft: nestingLevel * 8, }} - className={cx(styles.nodeContainer, { [styles.nodeSelected]: isSelected && isLeaf, })} + className={cx( + styles.nodeContainer, { + [styles.nodeSelected]: isSelected && isLeaf, + [styles.nodeRowEven]: index % 2 === 0, + } + )} > {isLeaf && Node} {!isLeaf && ( diff --git a/redisinsight/ui/src/components/virtual-tree/components/Node/styles.module.scss b/redisinsight/ui/src/components/virtual-tree/components/Node/styles.module.scss index 38e9aabe51..1af538d04f 100644 --- a/redisinsight/ui/src/components/virtual-tree/components/Node/styles.module.scss +++ b/redisinsight/ui/src/components/virtual-tree/components/Node/styles.module.scss @@ -1,5 +1,6 @@ .anchorTooltipNode { width: 100%; + height: 42px; display: inline-block; position: relative; } @@ -12,36 +13,62 @@ } } +.nodeRowEven { + background-color: var(--browserTableRowEven); +} + .nodeContent { display: flex; justify-content: space-between; cursor: pointer; - padding-right: 8px; + padding: 8px 16px; color: var(--euiTextSubduedColor) !important; - font: normal normal normal 13px/28px Graphik, sans-serif !important; + align-items: center; + font: + normal normal normal 13px/28px Graphik, + sans-serif !important; letter-spacing: -0.13px; + overflow: hidden; white-space: nowrap; + text-overflow: ellipsis; + height: 100%; &Open { color: var(--euiColorFullShade) !important; } + + .moveOnHover { + transition: transform ease 0.3s; + &.hide { + transform: translateX(-8px); + } + } + .showOnHover { + display: none; + &.show { + display: flex !important; + } + } + + &:hover { + .moveOnHover { + transform: translateX(-8px); + } + .showOnHover { + display: flex; + } + } } .nodeSelected { border-left-color: var(--euiColorPrimary) !important; - background-color: var(--browserComponentActive); + background-color: var(--browserComponentActive) !important; .nodeContent { color: var(--euiColorFullShade) !important; } } -.nodeName { - position: relative; - overflow: hidden; - text-overflow: ellipsis; -} - .nodeIcon { margin-right: 8px; @@ -60,10 +87,9 @@ } } -.approximate { +.approximate, +.keyCount { display: inline-block; - width: 36px; - text-align: end; } .options { @@ -71,3 +97,40 @@ font-size: 12px; font-weight: 300; } + +.keyType { + padding-right: 16px; + padding-left: 12px; + width: 126px; + min-width: 126px; +} + +.keyName { + flex-grow: 1; + position: relative; + overflow: hidden; + text-overflow: ellipsis; +} + +.keyTTL, +.approximate { + width: 86px; + min-width: 86px; + text-align: right; +} + +.keySize, +.keyCount { + width: 90px; + min-width: 90px; + text-align: right; +} + +.keyInfoLoading { + margin-top: 8px; + padding-left: 16px; +} + +.deletePopover { + max-width: 400px !important; +} diff --git a/redisinsight/ui/src/components/virtual-tree/interfaces.ts b/redisinsight/ui/src/components/virtual-tree/interfaces.ts index d2b1c21d26..717cd34fe4 100644 --- a/redisinsight/ui/src/components/virtual-tree/interfaces.ts +++ b/redisinsight/ui/src/components/virtual-tree/interfaces.ts @@ -1,5 +1,6 @@ import { FixedSizeNodeData } from 'react-vtree' -import { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys' +import { KeyTypes, ModulesKeyTypes } from 'uiSrc/constants' +import { RedisResponseBuffer, RedisString } from 'uiSrc/slices/interfaces' export interface TreeNode { children: TreeNode[] @@ -24,12 +25,11 @@ export interface NodeMetaData { keyCount: number, name: string, fullName: string, - setItems: (keys: any[]) => void, + shortName: string, updateStatusSelected: (fullName: string, keys: any) => void, updateStatusOpen: (name: string, value: boolean) => void, leafIcon: string, keyApproximate: number, - keys: any, isSelected: boolean, isOpenByDefault: boolean, } @@ -37,14 +37,24 @@ export interface NodeMetaData { export interface TreeData extends FixedSizeNodeData { isLeaf: boolean name: string + nameString: string + nameBuffer: RedisResponseBuffer + path: string keyCount: number keyApproximate: number fullName: string + shortName: string leafIcon: string - keys: IKeyPropTypes[] + type: KeyTypes | ModulesKeyTypes + ttl: number + size: number nestingLevel: number + deleting: boolean isSelected: boolean - setItems: (keys: any[]) => void + children?: TreeData[] updateStatusOpen: (fullName: string, value: boolean) => void - updateStatusSelected: (fullName: string, keys: IKeyPropTypes[]) => void + updateStatusSelected: (key: RedisString) => void + getMetadata: (key: RedisString, path: string) => void + onDelete: (key: RedisResponseBuffer) => void + onDeleteClicked: (type: KeyTypes | ModulesKeyTypes) => void } diff --git a/redisinsight/ui/src/components/virtual-tree/styles.module.scss b/redisinsight/ui/src/components/virtual-tree/styles.module.scss index 88595be6ce..64ec904730 100644 --- a/redisinsight/ui/src/components/virtual-tree/styles.module.scss +++ b/redisinsight/ui/src/components/virtual-tree/styles.module.scss @@ -36,3 +36,7 @@ top: 12px; left: 12px; } + +.progress { + z-index: 2; +} diff --git a/redisinsight/ui/src/constants/browser.ts b/redisinsight/ui/src/constants/browser.ts index c56384dafd..83e31d1232 100644 --- a/redisinsight/ui/src/constants/browser.ts +++ b/redisinsight/ui/src/constants/browser.ts @@ -1,6 +1,7 @@ -import { KeyValueFormat } from './keys' +import { KeyValueFormat, SortOrder } from './keys' export const DEFAULT_DELIMITER = ':' +export const DEFAULT_TREE_SORTING = SortOrder.ASC export const DEFAULT_SHOW_HIDDEN_RECOMMENDATIONS = false export const TEXT_UNPRINTABLE_CHARACTERS = { diff --git a/redisinsight/ui/src/helpers/constructKeysToTree.ts b/redisinsight/ui/src/helpers/constructKeysToTree.ts index 70b23501ee..18e10c6b4d 100644 --- a/redisinsight/ui/src/helpers/constructKeysToTree.ts +++ b/redisinsight/ui/src/helpers/constructKeysToTree.ts @@ -1,12 +1,14 @@ +import { SortOrder } from 'uiSrc/constants' import { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys' interface Props { items: IKeyPropTypes[] delimiter?: string + sorting?: SortOrder } export const constructKeysToTree = (props: Props): any[] => { - const { items: keys, delimiter = ':' } = props + const { items: keys, delimiter = ':', sorting = 'ASC' } = props const keysSymbol = `keys${delimiter}keys` const tree: any = {} @@ -20,11 +22,8 @@ export const constructKeysToTree = (props: Props): any[] => { nameSplitted.forEach((value:any, index: number) => { // create a key leaf if (index === lastIndex) { - if (currentNode[keysSymbol] === undefined) { - currentNode[keysSymbol] = {} - } - - currentNode[keysSymbol][name] = key + // eslint-disable-next-line prefer-object-spread + currentNode[name + keysSymbol] = Object.assign({}, key, { isLeaf: true }) } else if (currentNode[value] === undefined) { currentNode[value] = {} } @@ -47,39 +46,62 @@ export const constructKeysToTree = (props: Props): any[] => { return candidateId } - // FormatTreeData - const formatTreeData = (tree: any, previousKey = '', delimiter = ':') => { - const treeNodes = Reflect.ownKeys(tree) - - // sort Ungrouped Keys group to top - treeNodes.some((key, index) => { - if (key === keysSymbol) { - const temp = treeNodes[0] - treeNodes[0] = key - treeNodes[index] = temp - return true + // Folders should be always before leaves + const sortKeysAndFolder = (nodes: string[]) => { + nodes.sort((a, b) => { + // Custom sorting for items ending with "keys:keys" + if (a.endsWith(keysSymbol) && !b.endsWith(keysSymbol)) { + return 1 + } + if (!a.endsWith(keysSymbol) && b.endsWith(keysSymbol)) { + return -1 + } + + // Regular sorting + if (sorting === 'ASC') { + return a.localeCompare(b, 'en', { numeric: true }) + } + if (sorting === 'DESC') { + return b.localeCompare(a, 'en', { numeric: true }) } - return false + + return 0 }) + } + + // FormatTreeData + const formatTreeData = (tree: any, previousKey = '', delimiter = ':', prevIndex = '') => { + const treeNodes: string[] = Object.keys(tree) + + sortKeysAndFolder(treeNodes) - return treeNodes.map((key) => { + return treeNodes.map((key, index) => { const name = key?.toString() - const node: any = { name } + const node: any = { nameString: name } const tillNowKeyName = previousKey + name + delimiter + const path = prevIndex ? `${prevIndex}.${index}` : `${index}` // populate node with children nodes - if (key !== keysSymbol && Reflect.ownKeys(tree[key]).length > 0) { - node.children = formatTreeData(tree[key], tillNowKeyName, delimiter) - node.keyCount = node.children.reduce((a: any, b:any) => a + (b.keyCount || 0), 0) + if (!tree[key].isLeaf && Object.keys(tree[key]).length > 0) { + node.children = formatTreeData( + tree[key], + tillNowKeyName, + delimiter, + path, + ) + node.keyCount = node.children.reduce((a: any, b:any) => a + (b.keyCount || 1), 0) + node.keyApproximate = (node.keyCount / keys.length) * 100 } else { - // populate leaf with keys + // populate leaf + node.isLeaf = true node.children = [] - node.keys = tree[keysSymbol] ?? [] - node.keyCount = Object.keys(node.keys ?? [])?.length ?? 1 + node.nameString = name.slice(0, -keysSymbol.length) + node.nameBuffer = tree[key]?.name + node.shortName = node.nameString?.slice(previousKey.length) } + node.path = path node.fullName = tillNowKeyName - node.keyApproximate = (node.keyCount / keys.length) * 100 node.id = getUniqueId() return node }) diff --git a/redisinsight/ui/src/helpers/tests/constructKeysToTreeMockResult.ts b/redisinsight/ui/src/helpers/tests/constructKeysToTreeMockResult.ts index 0ced2705c5..53db673148 100644 --- a/redisinsight/ui/src/helpers/tests/constructKeysToTreeMockResult.ts +++ b/redisinsight/ui/src/helpers/tests/constructKeysToTreeMockResult.ts @@ -1,133 +1,118 @@ -import { DEFAULT_DELIMITER } from 'uiSrc/constants' -import { getTreeLeafField } from 'uiSrc/utils' - export const constructKeysToTreeMockResult = [ { - name: getTreeLeafField(DEFAULT_DELIMITER), - children: [], - keys: { - keys2: { - nameString: 'keys2', - type: 'hash', - ttl: -1, - size: 71 - }, - test1: { - nameString: 'test1', - type: 'hash', - ttl: -1, - size: 71 - }, - test2: { - nameString: 'test2', - type: 'hash', - ttl: -1, - size: 71 - }, - keys1: { - nameString: 'keys1', - type: 'hash', - ttl: -1, - size: 71 - } - }, - keyCount: 4, - fullName: `${getTreeLeafField(DEFAULT_DELIMITER)}:`, - keyApproximate: 40, - }, - { - name: 'keys', + nameString: 'empty', children: [ { - name: getTreeLeafField(DEFAULT_DELIMITER), - children: [], - keys: { - 'keys:1': { - nameString: 'keys:1', - type: 'hash', - ttl: -1, - size: 71 - }, - 'keys:3': { - nameString: 'keys:3', - type: 'hash', - ttl: -1, - size: 71 - }, - 'keys:2': { - nameString: 'keys:2', - type: 'hash', - ttl: -1, - size: 71 - } - }, - keyCount: 3, - fullName: `keys:${getTreeLeafField(DEFAULT_DELIMITER)}:`, - keyApproximate: 30, - }, - { - name: '1', + nameString: '', children: [ { - name: getTreeLeafField(DEFAULT_DELIMITER), + nameString: 'empty::test', + isLeaf: true, children: [], - keys: { - 'keys:1:2': { - nameString: 'keys:1:2', - type: 'hash', - ttl: -1, - size: 71 - }, - 'keys:1:1': { - nameString: 'keys:1:1', - type: 'hash', - ttl: -1, - size: 71 - } - }, - keyCount: 2, - fullName: `keys:1:${getTreeLeafField(DEFAULT_DELIMITER)}:`, - keyApproximate: 20, + shortName: 'test', + path: '0.0.0', + fullName: 'empty::empty::testkeys:keys:', } ], - keyCount: 2, - fullName: 'keys:1:', - keyApproximate: 20, + keyCount: 1, + keyApproximate: 10, + path: '0.0', + fullName: 'empty::', } ], - keyCount: 5, - fullName: 'keys:', - keyApproximate: 50, + keyCount: 1, + keyApproximate: 10, + path: '0', + fullName: 'empty:', }, { - name: 'empty', + nameString: 'keys', children: [ { - name: '', + nameString: '1', children: [ { - name: getTreeLeafField(DEFAULT_DELIMITER), + nameString: 'keys:1:1', + isLeaf: true, + children: [], + shortName: '1', + path: '1.0.0', + fullName: 'keys:1:keys:1:1keys:keys:', + }, + { + nameString: 'keys:1:2', + isLeaf: true, children: [], - keys: { - 'empty::test': { - nameString: 'empty::test', - type: 'hash', - ttl: -1, - size: 71 - } - }, - keyCount: 1, - fullName: `empty::${getTreeLeafField(DEFAULT_DELIMITER)}:`, - keyApproximate: 10, + shortName: '2', + path: '1.0.1', + fullName: 'keys:1:keys:1:2keys:keys:', } ], - keyCount: 1, - fullName: 'empty::', - keyApproximate: 10, + keyCount: 2, + keyApproximate: 20, + path: '1.0', + fullName: 'keys:1:', + }, + { + nameString: 'keys:1', + isLeaf: true, + children: [], + shortName: '1', + path: '1.1', + fullName: 'keys:keys:1keys:keys:', + }, + { + nameString: 'keys:2', + isLeaf: true, + children: [], + shortName: '2', + path: '1.2', + fullName: 'keys:keys:2keys:keys:', + }, + { + nameString: 'keys:3', + isLeaf: true, + children: [], + shortName: '3', + path: '1.3', + fullName: 'keys:keys:3keys:keys:', } ], - keyCount: 1, - fullName: 'empty:', - keyApproximate: 10, + keyCount: 5, + keyApproximate: 50, + path: '1', + fullName: 'keys:', + }, + { + nameString: 'keys1', + isLeaf: true, + children: [], + shortName: 'keys1', + path: '2', + fullName: 'keys1keys:keys:', + }, + { + nameString: 'keys2', + isLeaf: true, + children: [], + shortName: 'keys2', + path: '3', + fullName: 'keys2keys:keys:', + }, + { + nameString: 'test1', + isLeaf: true, + children: [], + shortName: 'test1', + path: '4', + fullName: 'test1keys:keys:', + }, + { + nameString: 'test2', + isLeaf: true, + children: [], + shortName: 'test2', + path: '5', + fullName: 'test2keys:keys:', } ] diff --git a/redisinsight/ui/src/pages/browser/components/browser-left-panel/BrowserLeftPanel.tsx b/redisinsight/ui/src/pages/browser/components/browser-left-panel/BrowserLeftPanel.tsx index df1ad59ddc..17b02065e1 100644 --- a/redisinsight/ui/src/pages/browser/components/browser-left-panel/BrowserLeftPanel.tsx +++ b/redisinsight/ui/src/pages/browser/components/browser-left-panel/BrowserLeftPanel.tsx @@ -56,6 +56,7 @@ const BrowserLeftPanel = (props: Props) => { searchMode, isSearched: patternIsSearched, filter, + deleting, } = useSelector(keysSelector) const { contextInstanceId } = useSelector(appContextSelector) const { @@ -154,6 +155,7 @@ const BrowserLeftPanel = (props: Props) => { selectKey={selectKey} loadMoreItems={loadMoreItems} onDelete={onDeleteKey} + deleting={deleting} onAddKeyPanel={handleAddKeyPanel} onBulkActionsPanel={handleBulkActionsPanel} /> 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 50fb490b05..9d9e911cb7 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 @@ -8,7 +8,6 @@ import BulkActions from 'uiSrc/pages/browser/components/bulk-actions' import CreateRedisearchIndex from 'uiSrc/pages/browser/components/create-redisearch-index/' import KeyDetailsWrapper from 'uiSrc/pages/browser/components/key-details/KeyDetailsWrapper' -import { updateBrowserTreeSelectedLeaf } from 'uiSrc/slices/app/context' import { keysDataSelector, keysSelector, @@ -16,9 +15,8 @@ import { toggleBrowserFullScreen } from 'uiSrc/slices/browser/keys' import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' -import { KeyViewType } from 'uiSrc/slices/interfaces/keys' import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { bufferToString, Nullable } from 'uiSrc/utils' +import { Nullable } from 'uiSrc/utils' export interface Props { selectedKey: Nullable @@ -91,10 +89,6 @@ const BrowserRightPanel = (props: Props) => { const handleEditKey = (key: RedisResponseBuffer, newKey: RedisResponseBuffer) => { setSelectedKey(newKey) - - if (viewType === KeyViewType.Tree) { - dispatch(updateBrowserTreeSelectedLeaf({ key: bufferToString(key), newKey: bufferToString(newKey) })) - } } const onEditKey = useCallback( diff --git a/redisinsight/ui/src/pages/browser/components/filter-key-type/FilterKeyType.tsx b/redisinsight/ui/src/pages/browser/components/filter-key-type/FilterKeyType.tsx index 2f131f4361..f75df5ae4c 100644 --- a/redisinsight/ui/src/pages/browser/components/filter-key-type/FilterKeyType.tsx +++ b/redisinsight/ui/src/pages/browser/components/filter-key-type/FilterKeyType.tsx @@ -18,6 +18,7 @@ import { isVersionHigherOrEquals } from 'uiSrc/utils' import { KeyViewType } from 'uiSrc/slices/interfaces/keys' import { FilterNotAvailable } from 'uiSrc/components' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { resetBrowserTree } from 'uiSrc/slices/app/context' import { FILTER_KEY_TYPE_OPTIONS } from './constants' import styles from './styles.module.scss' @@ -74,6 +75,7 @@ const FilterKeyType = () => { setTypeSelected(value) setIsSelectOpen(false) dispatch(setFilter(value === ALL_KEY_TYPES_VALUE ? null : value)) + viewType === KeyViewType.Tree && dispatch(resetBrowserTree()) dispatch( fetchKeys( { 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 e89c3a8f89..f72f3d0739 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 @@ -258,8 +258,8 @@ const KeyDetailsHeader = ({ setTTLIsEditing(false) setTTLIsHovering(false) - if (`${ttlProp}` !== ttlValue) { - onEditTTL(keyProp, +ttlValue) + if (`${ttlProp}` !== ttlValue && keyBuffer) { + onEditTTL(keyBuffer, +ttlValue) } } diff --git a/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx b/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx index 4407f212bf..f238dfcac5 100644 --- a/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx @@ -53,13 +53,13 @@ import VirtualTable from 'uiSrc/components/virtual-table/VirtualTable' import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces' import { KeyTypes, ModulesKeyTypes, TableCellAlignment, TableCellTextAlignment } from 'uiSrc/constants' import { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys' -import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { redisearchSelector } from 'uiSrc/slices/browser/redisearch' import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' -import NoKeysFound from 'uiSrc/pages/browser/components/no-keys-found' import { GetKeyInfoResponse } from 'apiSrc/modules/browser/dto' +import NoKeysMessage from '../no-keys-message' import styles from './styles.module.scss' export interface Props { @@ -155,42 +155,25 @@ const KeyList = forwardRef((props: Props, ref) => { controller.current?.abort() } + const NoItemsMessage = () => ( + + ) + const getNoItemsMessage = () => { if (isNotRendered.current) { return '' } - if (searchMode === SearchMode.Redisearch) { - if (!selectedIndex) { - return NoSelectedIndexText - } - - if (total === 0) { - return NoResultsFoundText - } - - if (isSearched) { - return keysState.scanned < total ? NoResultsFoundText : FullScanNoResultsFoundText - } - } - - if (total === 0) { - return () - } - - if (isSearched) { - return keysState.scanned < total ? ScanNoResultsFoundText : FullScanNoResultsFoundText - } - - if (isFiltered && keysState.scanned < total) { - return ScanNoResultsFoundText - } - if (itemsRef.current.length < keysState.keys.length) { return 'loading...' } - return NoResultsFoundText + return } const onLoadMoreItems = (props: { startIndex: number, stopIndex: number }) => { @@ -223,11 +206,7 @@ const KeyList = forwardRef((props: Props, ref) => { const handleDeletePopoverOpen = (index: Maybe, type: KeyTypes | ModulesKeyTypes) => { if (index !== deletePopoverIndex) { sendEventTelemetry({ - event: getBasedOnViewTypeEvent( - viewType, - TelemetryEvent.BROWSER_KEY_DELETE_CLICKED, - TelemetryEvent.TREE_VIEW_KEY_DELETE_CLICKED - ), + event: TelemetryEvent.BROWSER_KEY_DELETE_CLICKED, eventData: { databaseId: instanceId, keyType: type, diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.spec.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.spec.tsx index a38b56f5f9..60435f70ab 100644 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.spec.tsx @@ -2,17 +2,10 @@ import { cloneDeep } from 'lodash' import React from 'react' import { cleanup, - clearStoreActions, - fireEvent, mockedStore, render, - screen, - act, } from 'uiSrc/utils/test-utils' -import { setSearchMatch } from 'uiSrc/slices/browser/keys' import { KeysStoreData } from 'uiSrc/slices/interfaces/keys' -import { mockVirtualTreeResult } from 'uiSrc/components/virtual-tree/VirtualTree.spec' -import { setBrowserTreeNodesOpen, setBrowserTreeSelectedLeaf } from 'uiSrc/slices/app/context' import KeyTree from './KeyTree' let store: typeof mockedStore @@ -58,10 +51,6 @@ const propsMock = { selectKey: jest.fn(), } -const mockLeafKeys = { - test: { name: 'test', type: 'hash', ttl: -1, size: 9849176 } -} - const mockWebWorkerResult = [{ children: [{ children: [], @@ -84,7 +73,6 @@ const mockWebWorkerResult = [{ keyApproximate: 0.01, keyCount: 1, name: 'test', - keys: mockLeafKeys }] jest.mock('uiSrc/services', () => ({ @@ -93,13 +81,7 @@ jest.mock('uiSrc/services', () => ({ })) describe('KeyTree', () => { - it('Key tree delimiter should be in the document', () => { - render() - - expect(screen.getByTestId('tree-view-delimiter-btn')).toBeInTheDocument() - }) - - it('Tree view panel should be in the document', () => { + it.only('Tree view panel should be in the document', () => { const { container } = render() expect(container.querySelector('[data-test-subj="tree-view-panel"]')).toBeInTheDocument() @@ -110,42 +92,4 @@ describe('KeyTree', () => { expect(container.querySelector('[data-test-subj="key-list-panel"]')).toBeInTheDocument() }) - - it.skip('"setBrowserTreeNodesOpen" should be called for Open a node', async () => { - jest.useFakeTimers() - render() - - await act(() => { - jest.advanceTimersByTime(1000) - }) - - await act(() => { - fireEvent.click(screen.getByTestId(`node-item_${mockVirtualTreeResult?.[0]?.fullName}`)) - }) - - const expectedActions = [ - setBrowserTreeSelectedLeaf({}), - setBrowserTreeNodesOpen({}) - ] - - expect(clearStoreActions(store.getActions())).toEqual( - clearStoreActions(expectedActions) - ) - }) - - it.skip('"setSearchMatch" should be called after "onChange"', () => { - const searchTerm = 'a' - - render() - - fireEvent.change(screen.getByTestId('search-key'), { - target: { value: searchTerm }, - }) - - const expectedActions = [setSearchMatch(searchTerm)] - - expect(clearStoreActions(store.getActions())).toEqual( - clearStoreActions(expectedActions) - ) - }) }) diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx index 83401aff22..b289631390 100644 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx @@ -1,15 +1,13 @@ -import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useState, useTransition } from 'react' -import cx from 'classnames' -import { EuiResizableContainer } from '@elastic/eui' +import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { isEmpty } from 'lodash' +import cx from 'classnames' +import { useParams } from 'react-router-dom' import { appContextBrowserTree, resetBrowserTree, appContextDbConfig, setBrowserTreeNodesOpen, - setBrowserTreeSelectedLeaf } from 'uiSrc/slices/app/context' import { constructKeysToTree } from 'uiSrc/helpers' import VirtualTree from 'uiSrc/components/virtual-tree' @@ -17,17 +15,19 @@ import TreeViewSVG from 'uiSrc/assets/img/icons/treeview.svg' import { KeysStoreData } from 'uiSrc/slices/interfaces/keys' import { Nullable, bufferToString } from 'uiSrc/utils' import { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys' -import { KeyTypes } from 'uiSrc/constants' -import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' +import { KeyTypes, ModulesKeyTypes } from 'uiSrc/constants' +import { RedisResponseBuffer, RedisString } from 'uiSrc/slices/interfaces' +import { deleteKeyAction, selectedKeyDataSelector } from 'uiSrc/slices/browser/keys' +import { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry' import { GetKeyInfoResponse } from 'apiSrc/modules/browser/dto' -import KeyTreeDelimiter from './KeyTreeDelimiter' -import KeyList from '../key-list' +import NoKeysMessage from '../no-keys-message' import styles from './styles.module.scss' export interface Props { keysState: KeysStoreData loading: boolean + deleting: boolean commonFilterType: Nullable selectKey: ({ rowData }: { rowData: any }) => void loadMoreItems: ( @@ -54,24 +54,19 @@ const KeyTree = forwardRef((props: Props, ref) => { keysState, onDelete, commonFilterType, + deleting, onAddKeyPanel, - onBulkActionsPanel + onBulkActionsPanel, } = props - const firstPanelId = 'tree' - const secondPanelId = 'keys' - - const { panelSizes, openNodes, selectedLeaf } = useSelector(appContextBrowserTree) - const { treeViewDelimiter: delimiter = '' } = useSelector(appContextDbConfig) + const { instanceId } = useParams<{ instanceId: string }>() + const { openNodes } = useSelector(appContextBrowserTree) + const { treeViewDelimiter: delimiter = '', treeViewSort: sorting } = useSelector(appContextDbConfig) + const { nameString: selectedKeyName = null } = useSelector(selectedKeyDataSelector) ?? {} - const [,startTransition] = useTransition() - - const [statusSelected, setStatusSelected] = useState(selectedLeaf) const [statusOpen, setStatusOpen] = useState(openNodes) - const [sizes, setSizes] = useState(panelSizes) - const [keyListState, setKeyListState] = useState(keysState) const [constructingTree, setConstructingTree] = useState(false) - const [selectDefaultLeaf, setSelectDefaultLeaf] = useState(isEmpty(selectedLeaf)) + const [firstDataLoaded, setFirstDataLoaded] = useState(!!keysState.keys.length) const [items, setItems] = useState(parseKeyNames(keysState.keys ?? [])) const dispatch = useDispatch() @@ -83,19 +78,27 @@ const KeyTree = forwardRef((props: Props, ref) => { })) useEffect(() => { - updateKeysList() + openSelectedKey(selectedKeyName) }, []) useEffect(() => { setStatusOpen(openNodes) }, [openNodes]) - useEffect(() => { - setStatusSelected(selectedLeaf) - updateKeysList(Object.values(selectedLeaf)?.[0]) + // open all parents for selected key + const openSelectedKey = (selectedKeyName: Nullable = '') => { + if (selectedKeyName) { + const parts = selectedKeyName.split(delimiter) + const parents = parts.map((_, index) => parts.slice(0, index + 1).join(delimiter) + delimiter) - setSelectDefaultLeaf(isEmpty(selectedLeaf)) - }, [selectedLeaf]) + // remove key name from parents + parents.pop() + + setTimeout(() => { + parents.forEach((parent) => handleStatusOpen(parent, true)) + }, 0) + } + } useEffect(() => { setItems(parseKeyNames(keysState.keys)) @@ -106,8 +109,13 @@ const KeyTree = forwardRef((props: Props, ref) => { }, [keysState.keys]) useEffect(() => { + setFirstDataLoaded(true) setItems(parseKeyNames(keysState.keys)) - }, [delimiter, keysState.lastRefreshTime]) + }, [sorting, delimiter, keysState.lastRefreshTime]) + + useEffect(() => { + openSelectedKey(selectedKeyName) + }, [selectedKeyName]) const onLoadMoreItems = (props: { startIndex: number, stopIndex: number }) => { const formattedAllKeys = parseKeyNames(keysState.keys) @@ -117,38 +125,14 @@ const KeyTree = forwardRef((props: Props, ref) => { // select default leaf "Keys" after each change delimiter, filter or search const updateSelectedKeys = () => { dispatch(resetBrowserTree()) - - setTimeout(() => { - startTransition(() => { - setStatusSelected({}) - setSelectDefaultLeaf(true) - }) - }, 0) + openSelectedKey(selectedKeyName) } - const updateKeysList = (items:any = {}) => { - startTransition(() => { - const newState:KeysStoreData = { - ...keyListState, - keys: Object.values(items) - } - - setKeyListState(newState) - }) - } - - const onPanelWidthChange = useCallback((newSizes: any) => { - setSizes((prevSizes: any) => ({ - ...prevSizes, - ...newSizes, - })) - }, []) - const handleStatusOpen = (name: string, value:boolean) => { setStatusOpen((prevState) => { const newState = { ...prevState } // add or remove opened node - if (newState[name]) { + if (!value) { delete newState[name] } else { newState[name] = value @@ -159,85 +143,72 @@ const KeyTree = forwardRef((props: Props, ref) => { }) } - const handleStatusSelected = (fullName: string, keys: any) => { - dispatch(setBrowserTreeSelectedLeaf({ [fullName]: keys })) + const handleStatusSelected = (name: RedisString) => { + selectKey({ rowData: { name } }) + } + + const handleDeleteLeaf = (key: RedisResponseBuffer) => { + dispatch(deleteKeyAction(key, () => { + onDelete(key) + })) + } + + const handleDeleteClicked = (type: KeyTypes | ModulesKeyTypes) => { + sendEventTelemetry({ + event: TelemetryEvent.TREE_VIEW_KEY_DELETE_CLICKED, + eventData: { + databaseId: instanceId, + keyType: type, + source: 'keyList' + } + }) + } + + if (keysState.keys.length === 0) { + const NoItemsMessage = () => { + if (loading || !firstDataLoaded) { + return loading... + } + + return ( + + ) + } + + return ( +
+
+ +
+
+ ) } return ( -
+
-
- - {(EuiResizablePanel, EuiResizableButton) => ( - <> - -
-
- -
-
- setSelectDefaultLeaf(false)} - /> -
-
-
- - - - -
- -
-
- - )} -
-
+
) diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.spec.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.spec.tsx deleted file mode 100644 index 5df4b8cebd..0000000000 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.spec.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { cloneDeep } from 'lodash' -import React from 'react' -import { instance, mock } from 'ts-mockito' -import { DEFAULT_DELIMITER } from 'uiSrc/constants' -import { resetBrowserTree, setBrowserTreeDelimiter } from 'uiSrc/slices/app/context' -import { - cleanup, - clearStoreActions, - fireEvent, - mockedStore, - render, - screen, - act, -} from 'uiSrc/utils/test-utils' - -import KeyTreeDelimiter, { Props } from './KeyTreeDelimiter' - -const mockedProps = mock() -let store: typeof mockedStore -const INLINE_ITEM_EDITOR = 'inline-item-editor' -const INLINE_EDITOR_APPLY_BTN = 'apply-btn' -const DELIMITER_TRIGGER_BTN = 'tree-view-delimiter-btn' -const DELIMITER_INPUT = 'tree-view-delimiter-input' - -beforeEach(() => { - cleanup() - store = cloneDeep(mockedStore) - store.clearActions() -}) - -jest.mock('uiSrc/services', () => ({ - localStorageService: { - set: jest.fn(), - get: jest.fn(), - }, -})) - -describe('KeyTreeDelimiter', () => { - it('should render', () => { - expect(render()).toBeTruthy() - }) - - it('Delimiter button should be rendered', () => { - render() - - expect(screen.getByTestId(DELIMITER_TRIGGER_BTN)).toBeInTheDocument() - }) - - it('Delimiter input should be rendered after click on button', async () => { - render() - - await act(() => { - fireEvent.click(screen.getByTestId(DELIMITER_TRIGGER_BTN)) - }) - - expect(screen.getByTestId(DELIMITER_INPUT)).toBeInTheDocument() - }) - - it('"setBrowserTreeDelimiter" should be called after Apply change delimiter', async () => { - const value = 'val' - render() - - await act(() => { - fireEvent.click(screen.getByTestId(DELIMITER_TRIGGER_BTN)) - }) - - fireEvent.change(screen.getByTestId(INLINE_ITEM_EDITOR), { target: { value } }) - - await act(() => { - fireEvent.click(screen.getByTestId(INLINE_EDITOR_APPLY_BTN)) - }) - - const expectedActions = [ - setBrowserTreeDelimiter(value), - resetBrowserTree(), - ] - - expect(clearStoreActions(store.getActions())).toEqual( - clearStoreActions(expectedActions) - ) - }) - - it('"setBrowserTreeDelimiter" should be called with DEFAULT_DELIMITER after Apply change with empty input', async () => { - const value = '' - render() - - await act(() => { - fireEvent.click(screen.getByTestId(DELIMITER_TRIGGER_BTN)) - }) - - fireEvent.change(screen.getByTestId(INLINE_ITEM_EDITOR), { target: { value } }) - - await act(() => { - fireEvent.click(screen.getByTestId(INLINE_EDITOR_APPLY_BTN)) - }) - - const expectedActions = [ - setBrowserTreeDelimiter(DEFAULT_DELIMITER), - resetBrowserTree(), - ] - - expect(clearStoreActions(store.getActions())).toEqual( - clearStoreActions(expectedActions) - ) - }) -}) diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.tsx deleted file mode 100644 index 844065037a..0000000000 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React, { useState } from 'react' -import cx from 'classnames' -import { useDispatch, useSelector } from 'react-redux' -import { useParams } from 'react-router-dom' -import { EuiIcon, EuiPopover } from '@elastic/eui' - -import { replaceSpaces } from 'uiSrc/utils' -import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import InlineItemEditor from 'uiSrc/components/inline-item-editor' -import { DEFAULT_DELIMITER } from 'uiSrc/constants' -import { appContextDbConfig, resetBrowserTree, setBrowserTreeDelimiter } from 'uiSrc/slices/app/context' - -import styles from './styles.module.scss' - -export interface Props { - loading: boolean -} -const MAX_DELIMITER_LENGTH = 5 -const KeyTreeDelimiter = ({ loading }: Props) => { - const { instanceId = '' } = useParams<{ instanceId: string }>() - const { treeViewDelimiter: delimiter = '' } = useSelector(appContextDbConfig) - const [isPopoverOpen, setIsPopoverOpen] = useState(false) - - const dispatch = useDispatch() - - const onButtonClick = () => setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen) - const closePopover = () => setIsPopoverOpen(false) - - const button = ( -
{}} - data-testid="tree-view-delimiter-btn" - > - {replaceSpaces(delimiter)} - -
- ) - - const handleApplyDelimiter = (value: string) => { - sendEventTelemetry({ - event: TelemetryEvent.TREE_VIEW_DELIMITER_CHANGED, - eventData: { - databaseId: instanceId, - from: delimiter, - to: value || DEFAULT_DELIMITER - } - }) - closePopover() - dispatch(setBrowserTreeDelimiter(value || DEFAULT_DELIMITER)) - - dispatch(resetBrowserTree()) - } - - return ( -
- -
Delimiter
-
- closePopover()} - onApply={(value) => handleApplyDelimiter(value)} - /> -
-
-
- ) -} - -export default React.memo(KeyTreeDelimiter) diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/index.ts b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/index.ts deleted file mode 100644 index 88dc65dd7b..0000000000 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import KeyTreeDelimiter from './KeyTreeDelimiter' - -export default KeyTreeDelimiter diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/styles.module.scss b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/styles.module.scss deleted file mode 100644 index 3320d27cc7..0000000000 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/styles.module.scss +++ /dev/null @@ -1,68 +0,0 @@ -$selectDelimiterHeight: 18px; - -.anchorBtn { - width: 60px; - height: $selectDelimiterHeight; - margin-left: 6px; - padding-left: 6px; - margin-bottom: 10px; - border: 1px solid var(--separatorColor) !important; - border-radius: 4px; - font-size: 12px; - line-height: 16px; - - &Open { - border-bottom: 2px solid var(--euiColorPrimary) !important; - } - - svg { - position: absolute; - width: 12px !important; - height: 12px !important; - right: 5px; - top: 3px; - } -} - -.popoverWrapper { - height: 84px; - width: 182px; - padding: 12px 18px !important; - border: 1px solid var(--euiColorPrimary) !important; - background-color: var(--euiColorLightestShade) !important; - margin-top: -18px; - - :global(.euiPopover__panelArrow) { - &::before, - &::after { - content: none !important; - } - } - - .input { - display: inline-block; - width: 72px; - - input { - height: 36px !important; - border-radius: 4px; - background-color: var(--browserViewTypePassive) !important; - } - } -} - -.inputLabel { - display: inline-block; - width: 70px; - font-size: 14px; - color: var(--euiTextSubduedColor)!important; -} - -.input { - display: inline-block; - width: 72px; - - input { - background-color: var(--euiColorEmptyShade) !important; - } -} diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.spec.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.spec.tsx new file mode 100644 index 0000000000..591860f190 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.spec.tsx @@ -0,0 +1,154 @@ +import { cloneDeep } from 'lodash' +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { DEFAULT_DELIMITER, SortOrder } from 'uiSrc/constants' +import { resetBrowserTree, setBrowserTreeDelimiter, setBrowserTreeSort } from 'uiSrc/slices/app/context' +import { + cleanup, + clearStoreActions, + fireEvent, + mockedStore, + render, + screen, + act, + waitForEuiPopoverVisible, +} from 'uiSrc/utils/test-utils' + +import { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry' +import { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers' +import KeyTreeSettings, { Props } from './KeyTreeSettings' + +const mockedProps = mock() +let store: typeof mockedStore +const APPLY_BTN = 'tree-view-apply-btn' +const TREE_SETTINGS_TRIGGER_BTN = 'tree-view-settings-btn' +const SORTING_SELECT = 'tree-view-sorting-select' +const DELIMITER_INPUT = 'tree-view-delimiter-input' +const SORTING_DESC_ITEM = 'tree-view-sorting-item-DESC' + +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +jest.mock('uiSrc/services', () => ({ + localStorageService: { + set: jest.fn(), + get: jest.fn(), + }, +})) + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + +describe('KeyTreeDelimiter', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('Settings button should be rendered', () => { + render() + + expect(screen.getByTestId(TREE_SETTINGS_TRIGGER_BTN)).toBeInTheDocument() + }) + + it('Delimiter input and Sorting selector should be rendered after click on button', async () => { + render() + + await act(async () => { + fireEvent.click(screen.getByTestId(TREE_SETTINGS_TRIGGER_BTN)) + }) + await waitForEuiPopoverVisible() + + expect(screen.getByTestId(DELIMITER_INPUT)).toBeInTheDocument() + expect(screen.getByTestId(SORTING_SELECT)).toBeInTheDocument() + }) + + it('"setBrowserTreeDelimiter" and "setBrowserTreeSort" should be called after Apply change delimiter', async () => { + const sendEventTelemetryMock = jest.fn(); + (sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock) + const value = 'val' + render() + + await act(() => { + fireEvent.click(screen.getByTestId(TREE_SETTINGS_TRIGGER_BTN)) + }) + + await waitForEuiPopoverVisible() + + fireEvent.change(screen.getByTestId(DELIMITER_INPUT), { target: { value } }) + + await act(() => { + fireEvent.click(screen.getByTestId(SORTING_SELECT)) + }) + + await waitForEuiPopoverVisible() + + await act(() => { + fireEvent.click(screen.getByTestId(SORTING_DESC_ITEM)) + }) + + await act(() => { + fireEvent.click(screen.getByTestId(APPLY_BTN)) + }) + + const expectedActions = [ + setBrowserTreeDelimiter(value), + resetBrowserTree(), + setBrowserTreeSort(SortOrder.DESC), + resetBrowserTree(), + ] + + expect(clearStoreActions(store.getActions())).toEqual( + clearStoreActions(expectedActions) + ) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TREE_VIEW_DELIMITER_CHANGED, + eventData: { + databaseId: INSTANCE_ID_MOCK, + from: DEFAULT_DELIMITER, + to: value, + } + }) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TREE_VIEW_KEYS_SORTED, + eventData: { + databaseId: INSTANCE_ID_MOCK, + sorting: SortOrder.DESC, + } + }); + + (sendEventTelemetry as jest.Mock).mockRestore() + }) + + it('"setBrowserTreeDelimiter" should be called with DEFAULT_DELIMITER after Apply change with empty input', async () => { + const value = '' + render() + + await act(() => { + fireEvent.click(screen.getByTestId(TREE_SETTINGS_TRIGGER_BTN)) + }) + + await waitForEuiPopoverVisible() + + fireEvent.change(screen.getByTestId(DELIMITER_INPUT), { target: { value } }) + + await act(() => { + fireEvent.click(screen.getByTestId(APPLY_BTN)) + }) + + const expectedActions = [ + setBrowserTreeDelimiter(DEFAULT_DELIMITER), + resetBrowserTree(), + ] + + expect(clearStoreActions(store.getActions())).toEqual( + clearStoreActions(expectedActions) + ) + }) +}) diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.tsx new file mode 100644 index 0000000000..8febd940db --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.tsx @@ -0,0 +1,174 @@ +import React, { useCallback, useEffect, useState } from 'react' +import cx from 'classnames' +import { useDispatch, useSelector } from 'react-redux' +import { useParams } from 'react-router-dom' +import { EuiButton, EuiButtonIcon, EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPopover, EuiSuperSelect, EuiText } from '@elastic/eui' + +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { DEFAULT_DELIMITER, DEFAULT_TREE_SORTING, SortOrder } from 'uiSrc/constants' +import { + appContextDbConfig, + resetBrowserTree, + setBrowserTreeDelimiter, + setBrowserTreeSort, +} from 'uiSrc/slices/app/context' +import { ReactComponent as TreeViewSort } from 'uiSrc/assets/img/browser/treeViewSort.svg' + +import styles from './styles.module.scss' + +export interface Props { + loading: boolean +} +const MAX_DELIMITER_LENGTH = 5 +const sortOptions = [SortOrder.ASC, SortOrder.DESC].map((value) => ({ + value, + inputDisplay: ( + Key name {value} + ), +})) + +const KeyTreeSettings = ({ loading }: Props) => { + const { instanceId = '' } = useParams<{ instanceId: string }>() + const { treeViewDelimiter = '', treeViewSort = DEFAULT_TREE_SORTING } = useSelector(appContextDbConfig) + const [sorting, setSorting] = useState(treeViewSort) + const [delimiter, setDelimiter] = useState(treeViewDelimiter) + + const [isPopoverOpen, setIsPopoverOpen] = useState(false) + + const dispatch = useDispatch() + + useEffect(() => { + }, [treeViewSort]) + + useEffect(() => { + setDelimiter(treeViewDelimiter) + }, [treeViewDelimiter]) + + const onButtonClick = () => setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen) + const closePopover = () => { + setIsPopoverOpen(false) + setTimeout(() => { + resetStates() + }, 500) + } + + const resetStates = useCallback(() => { + setSorting(treeViewSort) + setDelimiter(treeViewDelimiter) + }, [treeViewSort, treeViewDelimiter]) + + const button = ( + + ) + + const handleApply = () => { + if (delimiter !== treeViewDelimiter) { + dispatch(setBrowserTreeDelimiter(delimiter || DEFAULT_DELIMITER)) + sendEventTelemetry({ + event: TelemetryEvent.TREE_VIEW_DELIMITER_CHANGED, + eventData: { + databaseId: instanceId, + from: treeViewDelimiter, + to: delimiter || DEFAULT_DELIMITER + } + }) + + dispatch(resetBrowserTree()) + } + + if (sorting !== treeViewSort) { + dispatch(setBrowserTreeSort(sorting)) + + sendEventTelemetry({ + event: TelemetryEvent.TREE_VIEW_KEYS_SORTED, + eventData: { + databaseId: instanceId, + sorting: sorting || DEFAULT_TREE_SORTING, + } + }) + + dispatch(resetBrowserTree()) + } + + setIsPopoverOpen(false) + } + + const onChangeSort = (value: SortOrder) => { + setSorting(value) + } + + return ( +
+ + + + Filters + + +
Delimiter
+ setDelimiter(e.target.value)} + aria-label="Title" + maxLength={MAX_DELIMITER_LENGTH} + data-testid="tree-view-delimiter-input" + /> +
+ +
+ + Sort by +
+ onChangeSort(value)} + data-testid="tree-view-sorting-select" + /> +
+ +
+ + Cancel + + + Apply + +
+
+
+
+
+ ) +} + +export default React.memo(KeyTreeSettings) diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/index.ts b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/index.ts new file mode 100644 index 0000000000..f748fe085d --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/index.ts @@ -0,0 +1,3 @@ +import KeyTreeSettings from './KeyTreeSettings' + +export default KeyTreeSettings diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/styles.module.scss b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/styles.module.scss new file mode 100644 index 0000000000..08bfec6e5c --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/styles.module.scss @@ -0,0 +1,84 @@ +.container { + margin-left: 8px; + margin-top: 1px; + + :global(.euiPopover), .anchorWrapper { + height: 100%; + } +} + +.anchorBtn { + width: 24px; + height: 100% !important; + + svg { + width: 18px !important; + height: 18px !important; + } +} + +.popoverWrapper { + height: 162px; + width: 300px; + padding: 14px 16px !important; + border: none !important; + background-color: var(--euiColorLightestShade) !important; + + :global { + .euiPopover__panelArrow { + &::before, + &::after { + border-bottom-color: var(--euiColorLightestShade) !important; + } + } + .euiFormControlLayout { + height: 26px !important; + width: auto; + } + } + + .input, .select { + width: 188px; + height: 24px !important; + font-size: 12px; + border-radius: 4px; + background-color: var(--browserViewTypePassive) !important; + } +} + +.label { + display: flex; + width: 66px; + font-size: 12px; + color: var(--euiTextSubduedColor)!important; +} + +.title { + font-size: 14px; + font-weight: 500; +} + +.row { + flex-direction: row !important; + align-items: center; + justify-content: space-between; +} + +.sortIcon { + margin-right: 4px; +} + +.selectItem { + min-height: 24px !important; +} + +.footer { + width: 100%; + display: flex; + justify-content: flex-end; + padding-top: 4px; + + button { + margin-left: 8px; + } +} \ No newline at end of file diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/index.ts b/redisinsight/ui/src/pages/browser/components/key-tree/index.ts index 24664563b0..dec23b355f 100644 --- a/redisinsight/ui/src/pages/browser/components/key-tree/index.ts +++ b/redisinsight/ui/src/pages/browser/components/key-tree/index.ts @@ -1,3 +1,8 @@ import KeyTree from './KeyTree' +import KeyTreeSettings from './KeyTreeSettings' export default KeyTree + +export { + KeyTreeSettings, +} diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/styles.module.scss b/redisinsight/ui/src/pages/browser/components/key-tree/styles.module.scss index 89dc51c251..c07d7ec3e0 100644 --- a/redisinsight/ui/src/pages/browser/components/key-tree/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/key-tree/styles.module.scss @@ -2,7 +2,7 @@ @import '@elastic/eui/src/components/table/mixins'; @import '@elastic/eui/src/global_styling/index'; -.page { +.container { height: 100%; overflow: hidden; } @@ -11,67 +11,27 @@ max-width: 372px !important; } +.noKeys { + @include euiScrollBar; + overflow: auto; + + text-align: center; + margin: auto; +} + .content { width: 100%; + display: flex; + flex-direction: column; height: 100%; background-color: var(--euiColorEmptyShade); border-top: 1px solid var(--euiColorLightShade); -} - -.body { - display: flex; - height: 100%; :global(.ReactVirtualized__Table__headerRow) { border: none !important; } } -.resizablePanelLeft { - border-right: 1px solid var(--euiColorLightShade); -} -.resizablePanelLeft, -.resizablePanelRight { - :global(.euiResizablePanel__content) { - padding-right: 0px !important; - } -} - -.resizableButton { - z-index: 1 !important; -} -.resizableButton:not(:hover) { - &::before, &::after { - width: 0 !important; - } -} - -.tree { - @include euiScrollBar; - - border-bottom: none; - position: relative; - padding-top: 6px; - - height: 100%; - - flex: 1; - display: flex; - flex-direction: column; -} - -.treeContent { - height: 100%; - position: relative; - width: 100%; -} - -.list { - height: 100%; - flex: 3; - min-width: 400px; -} - .filter { display: inline-block; width: 100px; diff --git a/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx b/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx index 24102f588f..04a3f9795f 100644 --- a/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx +++ b/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx @@ -117,6 +117,7 @@ const KeysHeader = (props: Props) => { } const handleRefreshKeys = () => { + dispatch(resetBrowserTree()) dispatch(fetchKeys( { searchMode, diff --git a/redisinsight/ui/src/pages/browser/components/no-keys-found/styles.module.scss b/redisinsight/ui/src/pages/browser/components/no-keys-found/styles.module.scss index 7d8902e5ea..73c3a47969 100644 --- a/redisinsight/ui/src/pages/browser/components/no-keys-found/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/no-keys-found/styles.module.scss @@ -1,7 +1,7 @@ .container { max-width: 400px; - margin: -10vh auto 0; + margin: auto; text-align: center; } diff --git a/redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.spec.tsx b/redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.spec.tsx new file mode 100644 index 0000000000..9dd48c0813 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.spec.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import { mock } from 'ts-mockito' +import { cloneDeep } from 'lodash' +import { render, mockedStore, cleanup } from 'uiSrc/utils/test-utils' + +import NoKeysMessage, { Props } from './NoKeysMessage' + +const mockedProps = mock() + +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +describe('NoKeysMessage', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.tsx b/redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.tsx new file mode 100644 index 0000000000..844966e77f --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.tsx @@ -0,0 +1,65 @@ +import React from 'react' + +import { useSelector } from 'react-redux' +import { SearchMode } from 'uiSrc/slices/interfaces/keys' + +import { + FullScanNoResultsFoundText, + NoResultsFoundText, + NoSelectedIndexText, + ScanNoResultsFoundText, +} from 'uiSrc/constants/texts' +import { keysSelector } from 'uiSrc/slices/browser/keys' +import { redisearchSelector } from 'uiSrc/slices/browser/redisearch' + +import NoKeysFound from '../no-keys-found' + +export interface Props { + total: number + scanned: number + onAddKeyPanel: (value: boolean) => void + onBulkActionsPanel: (value: boolean) => void +} + +const NoKeysMessage = (props:Props) => { + const { + total, + scanned, + onAddKeyPanel, + onBulkActionsPanel, + } = props + + const { selectedIndex } = useSelector(redisearchSelector) + const { isSearched: redisearchIsSearched } = useSelector(redisearchSelector) + const { isSearched: patternIsSearched, isFiltered, searchMode } = useSelector(keysSelector) + + if (searchMode === SearchMode.Redisearch) { + if (!selectedIndex) { + return NoSelectedIndexText + } + + if (total === 0) { + return NoResultsFoundText + } + + if (redisearchIsSearched) { + return scanned < total ? NoResultsFoundText : FullScanNoResultsFoundText + } + } + + if (total === 0) { + return () + } + + if (patternIsSearched) { + return scanned < total ? ScanNoResultsFoundText : FullScanNoResultsFoundText + } + + if (isFiltered && scanned < total) { + return ScanNoResultsFoundText + } + + return NoResultsFoundText +} + +export default NoKeysMessage diff --git a/redisinsight/ui/src/pages/browser/components/no-keys-message/index.ts b/redisinsight/ui/src/pages/browser/components/no-keys-message/index.ts new file mode 100644 index 0000000000..c3107e6843 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/no-keys-message/index.ts @@ -0,0 +1,3 @@ +import NoKeysMessage from './NoKeysMessage' + +export default NoKeysMessage diff --git a/redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.tsx b/redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.tsx index 8787ada41f..01824f0aa8 100644 --- a/redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.tsx +++ b/redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.tsx @@ -22,6 +22,7 @@ import { } from 'uiSrc/slices/browser/redisearch' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { resetBrowserTree } from 'uiSrc/slices/app/context' import styles from './styles.module.scss' const placeholders = { @@ -66,6 +67,8 @@ const SearchKeyList = () => { dispatch(setSearchMatch(match, searchMode)) + viewType === KeyViewType.Tree && dispatch(resetBrowserTree()) + dispatch(fetchKeys( { searchMode, diff --git a/redisinsight/ui/src/slices/app/context.ts b/redisinsight/ui/src/slices/app/context.ts index 1131492d2b..46d7ee2daf 100644 --- a/redisinsight/ui/src/slices/app/context.ts +++ b/redisinsight/ui/src/slices/app/context.ts @@ -1,14 +1,15 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { RelativeWidthSizes } from 'uiSrc/components/virtual-table/interfaces' import { ConfigDBStorageItem } from 'uiSrc/constants/storage' -import { getTreeLeafField, Nullable } from 'uiSrc/utils' +import { Nullable } from 'uiSrc/utils' import { BrowserStorageItem, DEFAULT_DELIMITER, DEFAULT_SLOWLOG_DURATION_UNIT, KeyTypes, DEFAULT_SHOW_HIDDEN_RECOMMENDATIONS, - DurationUnits, + SortOrder, + DEFAULT_TREE_SORTING, } from 'uiSrc/constants' import { localStorageService, setDBConfigStorageField } from 'uiSrc/services' import { RootState } from '../store' @@ -20,6 +21,7 @@ export const initialState: StateAppContext = { lastPage: '', dbConfig: { treeViewDelimiter: DEFAULT_DELIMITER, + treeViewSort: DEFAULT_TREE_SORTING, slowLogDurationUnit: DEFAULT_SLOWLOG_DURATION_UNIT, showHiddenRecommendations: DEFAULT_SHOW_HIDDEN_RECOMMENDATIONS, }, @@ -38,9 +40,8 @@ export const initialState: StateAppContext = { panelSizes: {}, tree: { delimiter: DEFAULT_DELIMITER, - panelSizes: {}, openNodes: {}, - selectedLeaf: {}, + selectedLeaf: null, }, bulkActions: { opened: false, @@ -106,6 +107,10 @@ const appContextSlice = createSlice({ state.dbConfig.treeViewDelimiter = payload setDBConfigStorageField(state.contextInstanceId, BrowserStorageItem.treeViewDelimiter, payload) }, + setBrowserTreeSort: (state, { payload }: PayloadAction) => { + state.dbConfig.treeViewSort = payload + setDBConfigStorageField(state.contextInstanceId, BrowserStorageItem.treeViewSort, payload) + }, setRecommendationsShowHidden: (state, { payload }: { payload: boolean }) => { state.dbConfig.showHiddenRecommendations = payload setDBConfigStorageField(state.contextInstanceId, BrowserStorageItem.showHiddenRecommendations, payload) @@ -137,38 +142,9 @@ const appContextSlice = createSlice({ setBrowserPanelSizes: (state, { payload }: { payload: any }) => { state.browser.panelSizes = payload }, - setBrowserTreeSelectedLeaf: (state, { payload }: { payload: any }) => { - state.browser.tree.selectedLeaf = payload - }, - updateBrowserTreeSelectedLeaf: (state, { payload }) => { - const { selectedLeaf, delimiter } = state.browser.tree - const [[selectedLeafField = '', keys = {}]] = Object.entries(selectedLeaf) - const [pattern] = selectedLeafField.split(getTreeLeafField(delimiter)) - - if (payload.key in keys) { - const isFitNewKey = payload.newKey?.startsWith?.(pattern) - && (pattern.split(delimiter)?.length === payload.newKey.split(delimiter)?.length) - - if (!isFitNewKey) { - delete keys[payload.key] - return - } - - keys[payload.newKey] = { - ...keys[payload.key], - name: payload.newKey - } - delete keys[payload.key] - } - - state.browser.tree.selectedLeaf[selectedLeafField] = keys - }, setBrowserTreeNodesOpen: (state, { payload }: { payload: { [key: string]: boolean; } }) => { state.browser.tree.openNodes = payload }, - setBrowserTreePanelSizes: (state, { payload }: { payload: any }) => { - state.browser.tree.panelSizes = payload - }, setWorkbenchScript: (state, { payload }: { payload: string }) => { state.workbench.script = payload }, @@ -197,7 +173,7 @@ const appContextSlice = createSlice({ localStorageService.set(BrowserStorageItem.isEnablementAreaMinimized, payload) }, resetBrowserTree: (state) => { - state.browser.tree.selectedLeaf = {} + state.browser.tree.selectedLeaf = null state.browser.tree.openNodes = {} }, setPubSubFieldsContext: (state, { payload }: { payload: { channel: string, message: string } }) => { @@ -240,12 +216,9 @@ export const { setBrowserRedisearchScrollPosition, setBrowserIsNotRendered, setBrowserPanelSizes, - setBrowserTreeSelectedLeaf, setBrowserTreeNodesOpen, setBrowserTreeDelimiter, - updateBrowserTreeSelectedLeaf, resetBrowserTree, - setBrowserTreePanelSizes, setWorkbenchScript, setWorkbenchVerticalPanelSizes, setLastPageContext, @@ -261,6 +234,7 @@ export const { setDbIndexState, setRecommendationsShowHidden, setLastTriggeredFunctionsPage, + setBrowserTreeSort, } = appContextSlice.actions // Selectors diff --git a/redisinsight/ui/src/slices/browser/keys.ts b/redisinsight/ui/src/slices/browser/keys.ts index 6044119189..30d9eb16f1 100644 --- a/redisinsight/ui/src/slices/browser/keys.ts +++ b/redisinsight/ui/src/slices/browser/keys.ts @@ -1023,6 +1023,39 @@ export function fetchKeysMetadata( } } +// Asynchronous thunk action +export function fetchKeysMetadataTree( + keys: RedisString[], + filter: Nullable, + signal?: AbortSignal, + onSuccessAction?: (data: GetKeyInfoResponse[]) => void, + onFailAction?: () => void +) { + return async (_dispatch: AppDispatch, stateInit: () => RootState) => { + try { + const state = stateInit() + const { data } = await apiService.post( + getUrl( + state.connections.instances?.connectedInstance?.id, + ApiEndpoints.KEYS_METADATA + ), + { keys: keys.map(([,nameBuffer]) => nameBuffer), type: filter || undefined }, + { params: { encoding: state.app.info.encoding }, signal } + ) + + const newData = data.map((key, i) => ({ ...key, path: keys[i][0] })) + + onSuccessAction?.(newData) + } catch (_err) { + if (!axios.isCancel(_err)) { + const error = _err as AxiosError + onFailAction?.() + console.error(error) + } + } + } +} + export function fetchPatternHistoryAction( onSuccess?: () => void, onFailed?: () => void, diff --git a/redisinsight/ui/src/slices/interfaces/app.ts b/redisinsight/ui/src/slices/interfaces/app.ts index e99ce6c1f3..40fb560410 100644 --- a/redisinsight/ui/src/slices/interfaces/app.ts +++ b/redisinsight/ui/src/slices/interfaces/app.ts @@ -1,8 +1,7 @@ import { AxiosError } from 'axios' import { RelativeWidthSizes } from 'uiSrc/components/virtual-table/interfaces' import { Nullable } from 'uiSrc/utils' -import { DurationUnits, FeatureFlags, ICommands } from 'uiSrc/constants' -import { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys' +import { DurationUnits, FeatureFlags, ICommands, SortOrder } from 'uiSrc/constants' import { GetServerInfoResponse } from 'apiSrc/modules/server/dto/server.dto' import { RedisString as RedisStringAPI } from 'apiSrc/common/constants/redis-string' @@ -50,6 +49,7 @@ export interface StateAppContext { lastPage: string dbConfig: { treeViewDelimiter: string + treeViewSort: SortOrder slowLogDurationUnit: DurationUnits showHiddenRecommendations: boolean } @@ -70,17 +70,10 @@ export interface StateAppContext { } tree: { delimiter: string - panelSizes: { - [key: string]: number - } openNodes: { [key: string]: boolean } - selectedLeaf: { - [key: string]: { - [key: string]: IKeyPropTypes - } - } + selectedLeaf: Nullable } bulkActions: { opened: boolean diff --git a/redisinsight/ui/src/slices/tests/app/context.spec.ts b/redisinsight/ui/src/slices/tests/app/context.spec.ts index 18a2ac60a8..75e05b99f6 100644 --- a/redisinsight/ui/src/slices/tests/app/context.spec.ts +++ b/redisinsight/ui/src/slices/tests/app/context.spec.ts @@ -27,11 +27,8 @@ import reducer, { setWorkbenchEAItemScrollTop, resetWorkbenchEASearch, setBrowserTreeNodesOpen, - setBrowserTreePanelSizes, resetBrowserTree, appContextBrowserTree, - setBrowserTreeSelectedLeaf, - updateBrowserTreeSelectedLeaf, setBrowserTreeDelimiter, setBrowserIsNotRendered, setBrowserRedisearchScrollPosition, @@ -477,72 +474,6 @@ describe('slices', () => { }) }) - describe('setBrowserTreeSelectedLeaf', () => { - it('should properly set selected keys in the tree', () => { - // Arrange - const selectedLeaf = { - [getTreeLeafField(DEFAULT_DELIMITER)]: { - test: { - name: 'test', - type: KeyTypes.Hash, - ttl: 123, - size: 123, - length: 321 - } - } - } - const prevState = { - ...initialState, - browser: { - ...initialState.browser, - tree: { - ...initialState.browser.tree, - selectedLeaf - } - }, - } - - const state = { - ...initialState.browser.tree, - selectedLeaf - } - - // Act - const nextState = reducer(prevState, setBrowserTreeSelectedLeaf(selectedLeaf)) - - // Assert - const rootState = Object.assign(initialStateDefault, { - app: { context: nextState }, - }) - - expect(appContextBrowserTree(rootState)).toEqual(state) - }) - }) - - describe('setBrowserTreePanelSizes', () => { - it('should properly set browser tree panel widths', () => { - // Arrange - const panelSizes = { - first: 50, - second: 400 - } - const state = { - ...initialState.browser.tree, - panelSizes - } - - // Act - const nextState = reducer(initialState, setBrowserTreePanelSizes(panelSizes)) - - // Assert - const rootState = Object.assign(initialStateDefault, { - app: { context: nextState }, - }) - - expect(appContextBrowserTree(rootState)).toEqual(state) - }) - }) - describe('setBrowserIsNotRendered', () => { it('should properly set browser is not rendered value', () => { // Arrange @@ -671,24 +602,14 @@ describe('slices', () => { openNodes: { test: true }, - selectedLeaf: { - [getTreeLeafField(DEFAULT_DELIMITER)]: { - test: { - name: 'test', - type: KeyTypes.Hash, - ttl: 123, - size: 123, - length: 321 - } - } - } + selectedLeaf: 'test', } }, } const state = { ...initialState.browser.tree, openNodes: {}, - selectedLeaf: {} + selectedLeaf: null } // Act @@ -703,119 +624,6 @@ describe('slices', () => { }) }) - describe('updateBrowserTreeSelectedLeaf', () => { - it('should properly update selected leaf and add a new fitted key', () => { - const payload = { - key: 'test', - newKey: 'test2' - } - // Arrange - const prevState = { - ...initialState, - browser: { - ...initialState.browser, - tree: { - ...initialState.browser.tree, - selectedLeaf: { - [getTreeLeafField(DEFAULT_DELIMITER)]: { - [payload.key]: { - name: payload.key, - type: KeyTypes.Hash, - ttl: 123, - size: 123, - length: 321 - } - } - } - } - }, - } - const state = { - ...initialState.browser.tree, - openNodes: {}, - selectedLeaf: { - [getTreeLeafField(DEFAULT_DELIMITER)]: { - [payload.newKey]: { - name: payload.newKey, - type: KeyTypes.Hash, - ttl: 123, - size: 123, - length: 321 - } } - } - } - - // Act - const nextState = reducer(prevState, updateBrowserTreeSelectedLeaf(payload)) - - // Assert - const rootState = Object.assign(initialStateDefault, { - app: { context: nextState }, - }) - - expect(appContextBrowserTree(rootState)).toEqual(state) - }) - it("should properly update selected leaf and remove old key (new key does't fit)", () => { - const payload = { - key: 'test', - newKey: 'test:2' - } - // Arrange - const prevState = { - ...initialState, - browser: { - ...initialState.browser, - tree: { - ...initialState.browser.tree, - selectedLeaf: { - [getTreeLeafField(DEFAULT_DELIMITER)]: { - [payload.key]: { - name: payload.key, - type: KeyTypes.Hash, - ttl: 123, - size: 123, - length: 321 - }, - test2: { - name: 'test2', - type: KeyTypes.Hash, - ttl: 123, - size: 123, - length: 321 - } - } - } - } - }, - } - const state = { - ...initialState.browser.tree, - openNodes: {}, - selectedLeaf: { - [getTreeLeafField(DEFAULT_DELIMITER)]: { - test2: { - name: 'test2', - type: KeyTypes.Hash, - ttl: 123, - size: 123, - length: 321 - }, - } - } - } - - // Act - const nextState = reducer(prevState, updateBrowserTreeSelectedLeaf(payload)) - - // Assert - const rootState = Object.assign(initialStateDefault, { - app: { context: nextState }, - }) - - expect(appContextBrowserTree(rootState)).toEqual(state) - }) - }) - describe('updateKeyDetailsSizes', () => { it('should properly update sizes', () => { // Arrange diff --git a/redisinsight/ui/src/telemetry/events.ts b/redisinsight/ui/src/telemetry/events.ts index f9a87f51c9..ae3a7d6134 100644 --- a/redisinsight/ui/src/telemetry/events.ts +++ b/redisinsight/ui/src/telemetry/events.ts @@ -162,6 +162,7 @@ export enum TelemetryEvent { TREE_VIEW_KEYS_SCANNED_WITH_FILTER_ENABLED = 'TREE_VIEW_KEYS_SCANNED_WITH_FILTER_ENABLED', TREE_VIEW_KEYS_ADDITIONALLY_SCANNED = 'TREE_VIEW_KEYS_ADDITIONALLY_SCANNED', TREE_VIEW_DELIMITER_CHANGED = 'TREE_VIEW_DELIMITER_CHANGED', + TREE_VIEW_KEYS_SORTED = 'TREE_VIEW_KEYS_SORTED', TREE_VIEW_KEY_ADDED = 'TREE_VIEW_KEY_ADDED', TREE_VIEW_KEY_LIST_AUTO_REFRESH_ENABLED = 'TREE_VIEW_KEY_LIST_AUTO_REFRESH_ENABLED', TREE_VIEW_KEY_LIST_AUTO_REFRESH_DISABLED = 'TREE_VIEW_KEY_LIST_AUTO_REFRESH_DISABLED', diff --git a/redisinsight/ui/src/utils/tests/tree.spec.ts b/redisinsight/ui/src/utils/tests/tree.spec.ts index 9eb64953d9..4c04963a49 100644 --- a/redisinsight/ui/src/utils/tests/tree.spec.ts +++ b/redisinsight/ui/src/utils/tests/tree.spec.ts @@ -1,5 +1,4 @@ -import { findTreeNode, getTreeLeafField } from 'uiSrc/utils' -import nodes from './nodes.json' +import { getTreeLeafField } from 'uiSrc/utils' const getTreeLeafFieldTests: any[] = [ [':', 'keys:keys'], @@ -19,23 +18,3 @@ describe('getTreeLeafField', () => { expect(result).toBe(expected) }) }) - -const findTreeNodeTests: any[] = [ - ['hash2:keys:keys:', 'id', null], - ['hash2:keys:keys:', 'fullName', nodes[1]?.children[0]], - ['hash:string:', 'fullName', nodes[0]?.children[1]], - ['hash:string:keys:keys:', 'fullName', nodes[0]?.children[1]?.children[0]], - ['0.g9y9ox4nau', 'id', nodes[0]?.children[0]], - ['hash2:keys:keys:', 'id', null], - ['uoeuoeuoe', 'id', null], - ['uoeuoeuoe', 'fullName', null], - ['hash2:', 'fullName', nodes[1]], -] - -describe('findTreeNode', () => { - it.each(findTreeNodeTests)('for input: %s (reply), should be output: %s', - (reply, key, expected) => { - const result = findTreeNode(nodes, reply, key) - expect(result).toBe(expected) - }) -}) diff --git a/redisinsight/ui/src/utils/tree.ts b/redisinsight/ui/src/utils/tree.ts index 817cd5d600..fc235737a7 100644 --- a/redisinsight/ui/src/utils/tree.ts +++ b/redisinsight/ui/src/utils/tree.ts @@ -1,26 +1 @@ -import { TreeNode } from 'uiSrc/components/virtual-tree' -import { Nullable } from './types' - export const getTreeLeafField = (delimiter = '') => `keys${delimiter}keys` - -export const findTreeNode = ( - data: TreeNode[], - value: string, - key = 'id', - tempObj: { found?: TreeNode } = {}, -): Nullable => { - if (value && data) { - // eslint-disable-next-line sonarjs/no-ignored-return - data.find((node) => { - if (node[key] === value) { - tempObj.found = node - return node - } - return findTreeNode(node.children, value, key, tempObj) - }) - if (tempObj.found) { - return tempObj.found - } - } - return null -} From 0408d83edfa906658e378eb7d25b03bbfa66362d Mon Sep 17 00:00:00 2001 From: zalenskiSofteq Date: Fri, 20 Oct 2023 18:54:13 +0200 Subject: [PATCH 02/12] #RI-4828 - Re-work the Tree view --- package.json | 8 +- patches/react-vtree+3.0.0-beta.3.patch | 29 +- .../virtual-tree/components/Node/Node.tsx | 356 ------------------ redisinsight/ui/src/constants/texts.tsx | 17 +- .../ui/src/helpers/constructKeysToTree.ts | 1 - .../tests/constructKeysToTreeMockResult.ts | 10 - .../browser/components/key-list/KeyList.tsx | 207 ++-------- .../components/key-list/styles.module.scss | 8 +- .../key-row-name/KeyRowName.spec.tsx | 29 ++ .../components/key-row-name/KeyRowName.tsx | 52 +++ .../browser/components/key-row-name/index.ts | 3 + .../key-row-name/styles.module.scss | 15 + .../key-row-size/KeyRowSize.spec.tsx | 49 +++ .../components/key-row-size/KeyRowSize.tsx | 135 +++++++ .../browser/components/key-row-size/index.ts | 3 + .../key-row-size/styles.module.scss | 10 + .../components/key-row-ttl/KeyRowTTL.spec.tsx | 49 +++ .../components/key-row-ttl/KeyRowTTL.tsx | 84 +++++ .../browser/components/key-row-ttl/index.ts | 3 + .../components/key-row-ttl/styles.module.scss | 10 + .../key-row-type/KeyRowType.spec.tsx | 37 ++ .../components/key-row-type/KeyRowType.tsx | 37 ++ .../browser/components/key-row-type/index.ts | 3 + .../key-row-type/styles.module.scss | 11 + .../components/key-tree/KeyTree.spec.tsx | 98 ++++- .../browser/components/key-tree/KeyTree.tsx | 6 +- .../no-keys-message/NoKeysMessage.spec.tsx | 157 +++++++- .../no-keys-message/NoKeysMessage.tsx | 3 +- .../virtual-tree/VirtualTree.spec.tsx | 0 .../components/virtual-tree/VirtualTree.tsx | 9 +- .../components/Node/Node.spec.tsx | 67 +++- .../virtual-tree/components/Node/Node.tsx | 198 ++++++++++ .../virtual-tree/components/Node/index.ts | 0 .../components/Node/styles.module.scss | 19 +- .../browser}/components/virtual-tree/index.ts | 0 .../components/virtual-tree/interfaces.ts | 2 - .../virtual-tree/styles.module.scss | 0 yarn.lock | 89 ++++- 38 files changed, 1207 insertions(+), 607 deletions(-) delete mode 100644 redisinsight/ui/src/components/virtual-tree/components/Node/Node.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.spec.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/key-row-name/index.ts create mode 100644 redisinsight/ui/src/pages/browser/components/key-row-name/styles.module.scss create mode 100644 redisinsight/ui/src/pages/browser/components/key-row-size/KeyRowSize.spec.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/key-row-size/KeyRowSize.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/key-row-size/index.ts create mode 100644 redisinsight/ui/src/pages/browser/components/key-row-size/styles.module.scss create mode 100644 redisinsight/ui/src/pages/browser/components/key-row-ttl/KeyRowTTL.spec.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/key-row-ttl/KeyRowTTL.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/key-row-ttl/index.ts create mode 100644 redisinsight/ui/src/pages/browser/components/key-row-ttl/styles.module.scss create mode 100644 redisinsight/ui/src/pages/browser/components/key-row-type/KeyRowType.spec.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/key-row-type/KeyRowType.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/key-row-type/index.ts create mode 100644 redisinsight/ui/src/pages/browser/components/key-row-type/styles.module.scss rename redisinsight/ui/src/{ => pages/browser}/components/virtual-tree/VirtualTree.spec.tsx (100%) rename redisinsight/ui/src/{ => pages/browser}/components/virtual-tree/VirtualTree.tsx (98%) rename redisinsight/ui/src/{ => pages/browser}/components/virtual-tree/components/Node/Node.spec.tsx (58%) create mode 100644 redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.tsx rename redisinsight/ui/src/{ => pages/browser}/components/virtual-tree/components/Node/index.ts (100%) rename redisinsight/ui/src/{ => pages/browser}/components/virtual-tree/components/Node/styles.module.scss (88%) rename redisinsight/ui/src/{ => pages/browser}/components/virtual-tree/index.ts (100%) rename redisinsight/ui/src/{ => pages/browser}/components/virtual-tree/interfaces.ts (97%) rename redisinsight/ui/src/{ => pages/browser}/components/virtual-tree/styles.module.scss (100%) diff --git a/package.json b/package.json index 2ea65190c0..6a764dddbd 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "package:mac": "yarn build:prod && electron-builder build --mac -p never", "package:mac:arm": "yarn build:prod && electron-builder build --mac --arm64 -p never", "package:linux": "yarn build:prod && electron-builder build --linux -p never", - "postinstall": "skip-postinstall || (electron-builder install-app-deps && cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./configs/webpack.config.renderer.dev.dll.ts && opencollective-postinstall && yarn-deduplicate yarn.lock)", + "postinstall": "patch-package && skip-postinstall || (electron-builder install-app-deps && cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./configs/webpack.config.renderer.dev.dll.ts && opencollective-postinstall && yarn-deduplicate yarn.lock)", "start": "ts-node ./scripts/check-port-in-use.js && yarn start:renderer", "start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./configs/webpack.config.renderer.dev.ts", "start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./configs/webpack.config.preload.dev.ts", @@ -41,8 +41,8 @@ "start:web": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./configs/webpack.config.web.dev.ts", "start:web:public": "cross-env PUBLIC_DEV=true NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./configs/webpack.config.web.dev.ts", "test": "jest ./redisinsight/ui -w 1", - "test:watch": "jest ./redisinsight/ui --watch -w 1", - "test:cov": "jest ./redisinsight/ui --coverage --no-cache --forceExit -w 3", + "test:watch": "jest ./redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.spec.tsx --watch -w 1", + "test:cov": "jest ./redisinsight/ui --silent --coverage --no-cache --forceExit -w 3", "test:cov:unit": "jest ./redisinsight/ui --group=-component --coverage -w 1", "test:cov:component": "jest ./redisinsight/ui --group=component --coverage -w 1", "type-check:ui": "tsc --project redisinsight/ui --noEmit" @@ -201,6 +201,8 @@ "msw": "^1.3.2", "node-sass": "^8.0.0", "opencollective-postinstall": "^2.0.3", + "patch-package": "^8.0.0", + "postinstall-postinstall": "^2.1.0", "react-hot-loader": "^4.13.0", "react-refresh": "^0.9.0", "redux-mock-store": "^1.5.4", diff --git a/patches/react-vtree+3.0.0-beta.3.patch b/patches/react-vtree+3.0.0-beta.3.patch index 7629fc04cc..cf1b00df37 100644 --- a/patches/react-vtree+3.0.0-beta.3.patch +++ b/patches/react-vtree+3.0.0-beta.3.patch @@ -1,12 +1,25 @@ +diff --git a/node_modules/react-vtree/dist/cjs/Tree.js b/node_modules/react-vtree/dist/cjs/Tree.js +index c46ce3e..879f0a6 100644 +--- a/node_modules/react-vtree/dist/cjs/Tree.js ++++ b/node_modules/react-vtree/dist/cjs/Tree.js +@@ -33,6 +33,7 @@ var Row = function Row(_ref) { + return /*#__PURE__*/_react.default.createElement(Node, Object.assign({ + isScrolling: isScrolling, + style: style, ++ index: index, + treeData: treeData + }, data)); + }; diff --git a/node_modules/react-vtree/dist/es/Tree.d.ts b/node_modules/react-vtree/dist/es/Tree.d.ts -index 5e7f57e..15c09d4 100644 +index 5e7f57e..b216b36 100644 --- a/node_modules/react-vtree/dist/es/Tree.d.ts +++ b/node_modules/react-vtree/dist/es/Tree.d.ts -@@ -24,6 +24,7 @@ export declare type NodePublicState = Readonly<{ +@@ -24,6 +24,8 @@ export declare type NodePublicState = Readonly<{ data: TData; setOpen: (state: boolean) => Promise; }> & { + index: number; ++ style: object; isOpen: boolean; }; export declare type NodeRecord> = Readonly<{ @@ -22,3 +35,15 @@ index 2b1c7c0..b22e873 100644 treeData: treeData }, data)); }; +diff --git a/node_modules/react-vtree/dist/lib/Tree.js b/node_modules/react-vtree/dist/lib/Tree.js +index fb824bd..6feba4e 100644 +--- a/node_modules/react-vtree/dist/lib/Tree.js ++++ b/node_modules/react-vtree/dist/lib/Tree.js +@@ -17,6 +17,7 @@ export const Row = ({ + return /*#__PURE__*/React.createElement(Node, Object.assign({ + isScrolling: isScrolling, + style: style, ++ index: index, + treeData: treeData + }, data)); + }; diff --git a/redisinsight/ui/src/components/virtual-tree/components/Node/Node.tsx b/redisinsight/ui/src/components/virtual-tree/components/Node/Node.tsx deleted file mode 100644 index 595e4af36c..0000000000 --- a/redisinsight/ui/src/components/virtual-tree/components/Node/Node.tsx +++ /dev/null @@ -1,356 +0,0 @@ -import React, { useEffect, useState } from 'react' -import { NodePublicState } from 'react-vtree/dist/es/Tree' -import cx from 'classnames' -import { isUndefined } from 'lodash' -import { - EuiIcon, - EuiToolTip, - keys as ElasticKeys, - EuiButtonIcon, - EuiLoadingContent, - EuiText, - EuiTextColor, - EuiPopover, - EuiSpacer, - EuiButton, -} from '@elastic/eui' - -import GroupBadge from 'uiSrc/components/group-badge' -import { - Maybe, - formatBytes, - formatLongName, - replaceSpaces, - truncateNumberToDuration, - truncateNumberToFirstUnit, - truncateTTLToSeconds, -} from 'uiSrc/utils' -import { KeyTypes, ModulesKeyTypes } from 'uiSrc/constants' -import { TreeData } from '../../interfaces' -import styles from './styles.module.scss' - -// Node component receives all the data we created in the `treeWalker` + -// internal openness state (`isOpen`), function to change internal openness -// `style` parameter that should be added to the root div. -const Node = ({ - data, - isOpen, - index, - style, - setOpen, -}: NodePublicState) => { - const { - id: nodeId, - isLeaf, - keyCount, - nestingLevel, - fullName, - nameBuffer, - path, - type, - ttl, - size, - deleting, - shortName, - nameString, - keyApproximate, - isSelected, - getMetadata, - onDelete, - onDeleteClicked, - updateStatusOpen, - updateStatusSelected, - } = data - - const [deletePopoverId, setDeletePopoverId] = useState>(undefined) - - useEffect(() => { - if (!size && isLeaf && nameBuffer) { - getMetadata(nameBuffer, path) - } - }, []) - - useEffect(() => { - if (isSelected && nameBuffer) { - updateStatusSelected?.(nameBuffer) - } - }, [isSelected]) - - const handleClick = () => { - if (isLeaf && !isSelected) { - updateStatusSelected?.(nameBuffer) - } - - updateStatusOpen?.(fullName, !isOpen) - !isLeaf && setOpen(!isOpen) - } - - const handleKeyDown = ({ key }: React.KeyboardEvent) => { - if (key === ElasticKeys.SPACE) { - handleClick() - } - } - - const handleDelete = () => { - onDelete(nameBuffer) - setDeletePopoverId(undefined) - } - - const handleDeletePopoverOpen = (index: Maybe, type: KeyTypes | ModulesKeyTypes) => { - if (index !== deletePopoverId) { - onDeleteClicked(type) - } - setDeletePopoverId(index !== deletePopoverId ? index : undefined) - } - - const Folder = () => ( - <> -
- - - {nameString} -
-
-
- {keyApproximate ? `${keyApproximate < 1 ? '<1' : Math.round(keyApproximate)}%` : '' } -
-
{keyCount ?? ''}
-
- - ) - - const Leaf = () => ( - <> - - - - - - ) - - const LeafType = () => ( - <> - {!type && } - {!!type &&
} - - ) - - const LeafName = () => { - // Better to cut the long string, because it could affect virtual scroll performance - const nameContent = replaceSpaces(shortName?.substring?.(0, 200)) - const nameTooltipContent = formatLongName(nameString) - - return ( -
- -
- - <>{nameContent} - -
-
-
- ) - } - - const LeafTTL = () => { - if (isUndefined(ttl)) { - return - } - if (ttl === -1) { - return ( - - No limit - - ) - } - return ( - -
- - {`${truncateTTLToSeconds(ttl)} s`} -
- {`(${truncateNumberToDuration(ttl)})`} - - )} - > - <>{truncateNumberToFirstUnit(ttl)} -
-
-
- ) - } - - const LeafSize = () => { - if (isUndefined(size)) { - return - } - - if (!size) { - return ( - - - - - ) - } - return ( - <> - -
- - {formatBytes(size, 3)} - - )} - > - <>{formatBytes(size, 0)} - -
-
- setDeletePopoverId(undefined)} - panelPaddingSize="l" - panelClassName={styles.deletePopover} - button={( - handleDeletePopoverOpen(nodeId, type)} - aria-label="Delete Key" - data-testid={`delete-key-btn-${nameString}`} - /> - )} - onClick={(e) => e.stopPropagation()} - > - <> - -

{formatLongName(nameString)}

- will be deleted. -
- - - Delete - - -
- - ) - } - - const Node = ( -
{}} - data-testid={`node-item_${fullName}`} - > - {!isLeaf && } - {isLeaf && } -
- ) - - const tooltipContent = ( - <> - {`${fullName}*`} -
- {`${keyCount} key(s) (${Math.round(keyApproximate * 100) / 100}%)`} - - ) - - return ( -
- {isLeaf && Node} - {!isLeaf && ( - - {Node} - - )} -
- ) -} - -export default Node diff --git a/redisinsight/ui/src/constants/texts.tsx b/redisinsight/ui/src/constants/texts.tsx index 1ce0beeca2..f8881ed31a 100644 --- a/redisinsight/ui/src/constants/texts.tsx +++ b/redisinsight/ui/src/constants/texts.tsx @@ -1,8 +1,19 @@ import React from 'react' import { EuiText, EuiSpacer } from '@elastic/eui' -export const NoResultsFoundText = (No results found.) -export const NoSelectedIndexText = (Select an index and enter a query to search per values of keys.) +export const NoResultsFoundText = ( + + No results found. + +) +export const NoSelectedIndexText = ( + + Select an index and enter a query to search per values of keys. + +) export const FullScanNoResultsFoundText = ( <> @@ -19,7 +30,7 @@ export const FullScanNoResultsFoundText = ( ) export const ScanNoResultsFoundText = ( <> - No results found. + No results found.
Use "Scan more" button to proceed or filter per exact Key Name to scan more efficiently. diff --git a/redisinsight/ui/src/helpers/constructKeysToTree.ts b/redisinsight/ui/src/helpers/constructKeysToTree.ts index 18e10c6b4d..d75559b94b 100644 --- a/redisinsight/ui/src/helpers/constructKeysToTree.ts +++ b/redisinsight/ui/src/helpers/constructKeysToTree.ts @@ -97,7 +97,6 @@ export const constructKeysToTree = (props: Props): any[] => { node.children = [] node.nameString = name.slice(0, -keysSymbol.length) node.nameBuffer = tree[key]?.name - node.shortName = node.nameString?.slice(previousKey.length) } node.path = path diff --git a/redisinsight/ui/src/helpers/tests/constructKeysToTreeMockResult.ts b/redisinsight/ui/src/helpers/tests/constructKeysToTreeMockResult.ts index 53db673148..0bce8a266c 100644 --- a/redisinsight/ui/src/helpers/tests/constructKeysToTreeMockResult.ts +++ b/redisinsight/ui/src/helpers/tests/constructKeysToTreeMockResult.ts @@ -9,7 +9,6 @@ export const constructKeysToTreeMockResult = [ nameString: 'empty::test', isLeaf: true, children: [], - shortName: 'test', path: '0.0.0', fullName: 'empty::empty::testkeys:keys:', } @@ -35,7 +34,6 @@ export const constructKeysToTreeMockResult = [ nameString: 'keys:1:1', isLeaf: true, children: [], - shortName: '1', path: '1.0.0', fullName: 'keys:1:keys:1:1keys:keys:', }, @@ -43,7 +41,6 @@ export const constructKeysToTreeMockResult = [ nameString: 'keys:1:2', isLeaf: true, children: [], - shortName: '2', path: '1.0.1', fullName: 'keys:1:keys:1:2keys:keys:', } @@ -57,7 +54,6 @@ export const constructKeysToTreeMockResult = [ nameString: 'keys:1', isLeaf: true, children: [], - shortName: '1', path: '1.1', fullName: 'keys:keys:1keys:keys:', }, @@ -65,7 +61,6 @@ export const constructKeysToTreeMockResult = [ nameString: 'keys:2', isLeaf: true, children: [], - shortName: '2', path: '1.2', fullName: 'keys:keys:2keys:keys:', }, @@ -73,7 +68,6 @@ export const constructKeysToTreeMockResult = [ nameString: 'keys:3', isLeaf: true, children: [], - shortName: '3', path: '1.3', fullName: 'keys:keys:3keys:keys:', } @@ -87,7 +81,6 @@ export const constructKeysToTreeMockResult = [ nameString: 'keys1', isLeaf: true, children: [], - shortName: 'keys1', path: '2', fullName: 'keys1keys:keys:', }, @@ -95,7 +88,6 @@ export const constructKeysToTreeMockResult = [ nameString: 'keys2', isLeaf: true, children: [], - shortName: 'keys2', path: '3', fullName: 'keys2keys:keys:', }, @@ -103,7 +95,6 @@ export const constructKeysToTreeMockResult = [ nameString: 'test1', isLeaf: true, children: [], - shortName: 'test1', path: '4', fullName: 'test1keys:keys:', }, @@ -111,7 +102,6 @@ export const constructKeysToTreeMockResult = [ nameString: 'test2', isLeaf: true, children: [], - shortName: 'test2', path: '5', fullName: 'test2keys:keys:', } diff --git a/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx b/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx index f238dfcac5..6df981124a 100644 --- a/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx @@ -4,34 +4,13 @@ import cx from 'classnames' import { useParams } from 'react-router-dom' import { debounce, findIndex, isUndefined, reject } from 'lodash' -import { - EuiText, - EuiToolTip, - EuiTextColor, - EuiLoadingContent, - EuiPopover, - EuiButton, - EuiButtonIcon, EuiSpacer, -} from '@elastic/eui' import { CellMeasurerCache } from 'react-virtualized' import { - formatBytes, - truncateNumberToDuration, - truncateNumberToFirstUnit, - truncateTTLToSeconds, - replaceSpaces, - formatLongName, bufferToString, bufferFormatRangeItems, Nullable, Maybe, } from 'uiSrc/utils' -import { - NoResultsFoundText, - FullScanNoResultsFoundText, - ScanNoResultsFoundText, - NoSelectedIndexText, -} from 'uiSrc/constants/texts' import { deleteKeyAction, fetchKeysMetadata, @@ -46,7 +25,6 @@ import { setBrowserIsNotRendered, setBrowserRedisearchScrollPosition, } from 'uiSrc/slices/app/context' -import { GroupBadge } from 'uiSrc/components' import { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api' import { KeysStoreData, SearchMode } from 'uiSrc/slices/interfaces/keys' import VirtualTable from 'uiSrc/components/virtual-table/VirtualTable' @@ -54,8 +32,11 @@ import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces' import { KeyTypes, ModulesKeyTypes, TableCellAlignment, TableCellTextAlignment } from 'uiSrc/constants' import { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { redisearchSelector } from 'uiSrc/slices/browser/redisearch' import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' +import KeyRowTTL from 'uiSrc/pages/browser/components/key-row-ttl' +import KeyRowSize from 'uiSrc/pages/browser/components/key-row-size' +import KeyRowName from 'uiSrc/pages/browser/components/key-row-name' +import KeyRowType from 'uiSrc/pages/browser/components/key-row-type' import { GetKeyInfoResponse } from 'apiSrc/modules/browser/dto' @@ -101,9 +82,8 @@ const KeyList = forwardRef((props: Props, ref) => { const { instanceId = '' } = useParams<{ instanceId: string }>() const selectedKey = useSelector(selectedKeySelector) - const { total, nextCursor, previousResultCount } = useSelector(keysDataSelector) - const { isSearched, isFiltered, viewType, searchMode, deleting } = useSelector(keysSelector) - const { selectedIndex } = useSelector(redisearchSelector) + const { nextCursor, previousResultCount } = useSelector(keysDataSelector) + const { isSearched, isFiltered, searchMode, deleting } = useSelector(keysSelector) const { keyList: { isNotRendered: isNotRenderedContext } } = useSelector(appContextBrowser) const [, rerender] = useState({}) @@ -301,10 +281,8 @@ const KeyList = forwardRef((props: Props, ref) => { label: 'Type', absoluteWidth: 'auto', minWidth: 126, - render: (cellData: any, { nameString: name }: any) => ( - isUndefined(cellData) - ? - : + render: (cellData: any, { nameString }: any) => ( + ) }, { @@ -312,36 +290,9 @@ const KeyList = forwardRef((props: Props, ref) => { label: 'Key', minWidth: 94, truncateText: true, - render: (cellData: string) => { - if (isUndefined(cellData)) { - return ( - - ) - } - // Better to cut the long string, because it could affect virtual scroll performance - const name = cellData || '' - const cellContent = replaceSpaces(name?.substring(0, 200)) - const tooltipContent = formatLongName(name) - return ( - -
- - <>{cellContent} - -
-
- ) - } + render: (cellData: string) => ( + + ) }, { id: 'ttl', @@ -350,48 +301,9 @@ const KeyList = forwardRef((props: Props, ref) => { minWidth: 86, truncateText: true, alignment: TableCellAlignment.Right, - render: (cellData: number, { nameString: name }: IKeyPropTypes, _expanded, rowIndex) => { - if (isUndefined(cellData)) { - return - } - if (cellData === -1) { - return ( - - No limit - - ) - } - return ( - -
- - {`${truncateTTLToSeconds(cellData)} s`} -
- {`(${truncateNumberToDuration(cellData)})`} - - )} - > - <>{truncateNumberToFirstUnit(cellData)} -
-
-
- ) - }, + render: (cellData: number, { nameString }: IKeyPropTypes, _expanded, rowIndex) => ( + + ) }, { id: 'size', @@ -402,84 +314,23 @@ const KeyList = forwardRef((props: Props, ref) => { textAlignment: TableCellTextAlignment.Right, render: ( cellData: number, - { nameString: name, type, name: bufferName }: IKeyPropTypes, + { nameString, type, name: bufferName }: IKeyPropTypes, _expanded, rowIndex - ) => { - if (isUndefined(cellData)) { - return - } - - if (!cellData) { - return ( - - - - - ) - } - return ( - <> - -
- - {formatBytes(cellData, 3)} - - )} - > - <>{formatBytes(cellData, 0)} - -
-
- setDeletePopoverIndex(undefined)} - panelPaddingSize="l" - panelClassName={styles.deletePopover} - button={( - handleDeletePopoverOpen(rowIndex, type)} - aria-label="Delete Key" - data-testid={`delete-key-btn-${name}`} - /> - )} - onClick={(e) => e.stopPropagation()} - > - <> - -

{formatLongName(name)}

- will be deleted. -
- - handleRemoveKey(bufferName)} - data-testid="submit-delete-key" - > - Delete - - -
- - ) - } + ) => ( + + ) }, ] diff --git a/redisinsight/ui/src/pages/browser/components/key-list/styles.module.scss b/redisinsight/ui/src/pages/browser/components/key-list/styles.module.scss index 9b75dab2e9..ddefddc587 100644 --- a/redisinsight/ui/src/pages/browser/components/key-list/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/key-list/styles.module.scss @@ -27,19 +27,19 @@ :global { .ReactVirtualized__Table__row { .ReactVirtualized__Table__rowColumn { - .moveOnHover { + .moveOnHoverKey { transition: transform ease 0.3s; &.hide { transform: translateX(-8px) } } - .showOnHover { + .showOnHoverKey { display: none; &.show { display: block !important; }} } &:hover { .ReactVirtualized__Table__rowColumn { - .moveOnHover { transform: translateX(-8px) } - .showOnHover { display: block; } + .moveOnHoverKey { transform: translateX(-8px) } + .showOnHoverKey { display: block !important; } } } } diff --git a/redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.spec.tsx b/redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.spec.tsx new file mode 100644 index 0000000000..8ee2f086ea --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.spec.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' + +import KeyRowName, { Props } from './KeyRowName' + +const mockedProps = mock() + +const loadingTestId = 'name-loading' + +describe('KeyRowName', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should render Loading if no nameString', () => { + const { queryByTestId } = render() + + expect(queryByTestId(loadingTestId)).toBeInTheDocument() + }) + + it('content should be no more than 200 symbols', () => { + const longName = Array.from({ length: 250 }, () => '1').join('') + const { queryByTestId } = render() + + expect(queryByTestId(loadingTestId)).not.toBeInTheDocument() + expect(queryByTestId(`key-${longName}`)).toHaveTextContent(longName.slice(0, 200)) + }) +}) diff --git a/redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.tsx b/redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.tsx new file mode 100644 index 0000000000..5dfc1df372 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import { + EuiLoadingContent, + EuiText, + EuiToolTip, +} from '@elastic/eui' +import { isUndefined } from 'lodash' + +import { Maybe, formatLongName, replaceSpaces } from 'uiSrc/utils' +import styles from './styles.module.scss' + +export interface Props { + nameString: Maybe +} + +const KeyRowName = (props: Props) => { + const { nameString } = props + + if (isUndefined(nameString)) { + return ( + + ) + } + + // Better to cut the long string, because it could affect virtual scroll performance + const nameContent = replaceSpaces(nameString?.substring?.(0, 200)) + const nameTooltipContent = formatLongName(nameString) + + return ( +
+ +
+ + <>{nameContent} + +
+
+
+ ) +} + +export default KeyRowName diff --git a/redisinsight/ui/src/pages/browser/components/key-row-name/index.ts b/redisinsight/ui/src/pages/browser/components/key-row-name/index.ts new file mode 100644 index 0000000000..03ecaaf22a --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-name/index.ts @@ -0,0 +1,3 @@ +import KeyRowName from './KeyRowName' + +export default KeyRowName diff --git a/redisinsight/ui/src/pages/browser/components/key-row-name/styles.module.scss b/redisinsight/ui/src/pages/browser/components/key-row-name/styles.module.scss new file mode 100644 index 0000000000..8157e074f4 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-name/styles.module.scss @@ -0,0 +1,15 @@ +.keyInfoLoading { + width: 70%; + margin-top: 7px; +} + +.keyName { + flex-grow: 1; + position: relative; + overflow: hidden; + text-overflow: ellipsis; + + :global(.euiTextColor) { + max-width: 100%; + } +} diff --git a/redisinsight/ui/src/pages/browser/components/key-row-size/KeyRowSize.spec.tsx b/redisinsight/ui/src/pages/browser/components/key-row-size/KeyRowSize.spec.tsx new file mode 100644 index 0000000000..69e2da224c --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-size/KeyRowSize.spec.tsx @@ -0,0 +1,49 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' + +import { render } from 'uiSrc/utils/test-utils' +import { formatBytes } from 'uiSrc/utils' +import KeyRowSize, { Props } from './KeyRowSize' + +const mockedProps = mock() +const loadingTestId = 'size-loading_' +const nameString = 'name' + +describe('KeyRowSize', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should render Loading if no size', () => { + const { queryByTestId } = render() + + expect(queryByTestId(loadingTestId + nameString)).toBeInTheDocument() + }) + + it('should render "-" if size is empty', () => { + const { queryByTestId } = render() + + expect(queryByTestId(loadingTestId + nameString)).not.toBeInTheDocument() + expect(queryByTestId(`size-${nameString}`)).toHaveTextContent('-') + }) + + it('should render formatted size', () => { + const size = 123123123 + const { queryByTestId } = render() + + expect(queryByTestId(loadingTestId + nameString)).not.toBeInTheDocument() + expect(queryByTestId(`size-${nameString}`)).toHaveTextContent(formatBytes(size, 0) as string) + }) +}) diff --git a/redisinsight/ui/src/pages/browser/components/key-row-size/KeyRowSize.tsx b/redisinsight/ui/src/pages/browser/components/key-row-size/KeyRowSize.tsx new file mode 100644 index 0000000000..e0dce54212 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-size/KeyRowSize.tsx @@ -0,0 +1,135 @@ +import React from 'react' +import cx from 'classnames' +import { + EuiButton, + EuiButtonIcon, + EuiLoadingContent, + EuiPopover, + EuiSpacer, + EuiText, + EuiToolTip, +} from '@elastic/eui' +import { isUndefined } from 'lodash' + +import { Maybe, formatBytes, formatLongName } from 'uiSrc/utils' +import { KeyTypes, ModulesKeyTypes } from 'uiSrc/constants' +import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' +import styles from './styles.module.scss' + +export interface Props { + size: Maybe + deletePopoverId: Maybe + rowId: number | string + nameString: string + type: KeyTypes | ModulesKeyTypes + deleting: boolean + nameBuffer: RedisResponseBuffer + setDeletePopoverId: (id: any) => void + handleDeletePopoverOpen: (id: any, type: KeyTypes | ModulesKeyTypes) => void + handleDelete: (key: RedisResponseBuffer) => void +} + +const KeyRowSize = (props: Props) => { + const { + size, + nameString, + nameBuffer, + deletePopoverId, + deleting, + rowId, + type, + setDeletePopoverId, + handleDeletePopoverOpen, + handleDelete, + } = props + + if (isUndefined(size)) { + return ( + + ) + } + + if (!size) { + return ( + + - + + ) + } + return ( + <> + +
+ + {formatBytes(size, 3)} + + )} + > + <>{formatBytes(size, 0)} + +
+
+ setDeletePopoverId(undefined)} + panelPaddingSize="l" + panelClassName={styles.deletePopover} + button={( + handleDeletePopoverOpen(rowId, type)} + aria-label="Delete Key" + data-testid={`delete-key-btn-${nameString}`} + /> + )} + onClick={(e) => e.stopPropagation()} + > + <> + +

{formatLongName(nameString)}

+ will be deleted. +
+ + handleDelete(nameBuffer)} + data-testid="submit-delete-key" + > + Delete + + +
+ + ) +} + +export default KeyRowSize diff --git a/redisinsight/ui/src/pages/browser/components/key-row-size/index.ts b/redisinsight/ui/src/pages/browser/components/key-row-size/index.ts new file mode 100644 index 0000000000..f139af942d --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-size/index.ts @@ -0,0 +1,3 @@ +import KeyRowSize from './KeyRowSize' + +export default KeyRowSize diff --git a/redisinsight/ui/src/pages/browser/components/key-row-size/styles.module.scss b/redisinsight/ui/src/pages/browser/components/key-row-size/styles.module.scss new file mode 100644 index 0000000000..a8cddcd317 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-size/styles.module.scss @@ -0,0 +1,10 @@ +.keyInfoLoading { + margin-top: 8px; + padding-left: 16px; +} + +.keySize { + width: 90px; + min-width: 90px; + text-align: right; +} diff --git a/redisinsight/ui/src/pages/browser/components/key-row-ttl/KeyRowTTL.spec.tsx b/redisinsight/ui/src/pages/browser/components/key-row-ttl/KeyRowTTL.spec.tsx new file mode 100644 index 0000000000..24a0d7ba8b --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-ttl/KeyRowTTL.spec.tsx @@ -0,0 +1,49 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' + +import { render } from 'uiSrc/utils/test-utils' +import { truncateNumberToFirstUnit } from 'uiSrc/utils' +import KeyRowTTL, { Props } from './KeyRowTTL' + +const mockedProps = mock() +const loadingTestId = 'ttl-loading_' +const nameString = 'name' + +describe('KeyRowTTL', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should render Loading if no ttl', () => { + const { queryByTestId } = render() + + expect(queryByTestId(loadingTestId + nameString)).toBeInTheDocument() + }) + + it('should render "No limit" if ttl is -1', () => { + const { queryByTestId } = render() + + expect(queryByTestId(loadingTestId + nameString)).not.toBeInTheDocument() + expect(queryByTestId(`ttl-${nameString}`)).toHaveTextContent('No limit') + }) + + it('should render formatted ttl', () => { + const ttl = 123123123 + const { queryByTestId } = render() + + expect(queryByTestId(loadingTestId + nameString)).not.toBeInTheDocument() + expect(queryByTestId(`ttl-${nameString}`)).toHaveTextContent(truncateNumberToFirstUnit(ttl)) + }) +}) diff --git a/redisinsight/ui/src/pages/browser/components/key-row-ttl/KeyRowTTL.tsx b/redisinsight/ui/src/pages/browser/components/key-row-ttl/KeyRowTTL.tsx new file mode 100644 index 0000000000..1c52434606 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-ttl/KeyRowTTL.tsx @@ -0,0 +1,84 @@ +import React from 'react' +import cx from 'classnames' +import { + EuiLoadingContent, + EuiText, + EuiTextColor, + EuiToolTip, +} from '@elastic/eui' +import { isUndefined } from 'lodash' + +import { + Maybe, + truncateNumberToDuration, + truncateNumberToFirstUnit, + truncateTTLToSeconds, +} from 'uiSrc/utils' +import styles from './styles.module.scss' + +export interface Props { + ttl: Maybe + deletePopoverId: Maybe + rowId: number | string + nameString: string +} + +const KeyRowTTL = (props: Props) => { + const { ttl, nameString, deletePopoverId, rowId } = props + + if (isUndefined(ttl)) { + return ( + + ) + } + if (ttl === -1) { + return ( + + No limit + + ) + } + return ( + +
+ + {`${truncateTTLToSeconds(ttl)} s`} +
+ {`(${truncateNumberToDuration(ttl)})`} + + )} + > + <>{truncateNumberToFirstUnit(ttl)} +
+
+
+ ) +} + +export default KeyRowTTL diff --git a/redisinsight/ui/src/pages/browser/components/key-row-ttl/index.ts b/redisinsight/ui/src/pages/browser/components/key-row-ttl/index.ts new file mode 100644 index 0000000000..bd31d8b0a6 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-ttl/index.ts @@ -0,0 +1,3 @@ +import KeyRowTTL from './KeyRowTTL' + +export default KeyRowTTL diff --git a/redisinsight/ui/src/pages/browser/components/key-row-ttl/styles.module.scss b/redisinsight/ui/src/pages/browser/components/key-row-ttl/styles.module.scss new file mode 100644 index 0000000000..d05353485f --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-ttl/styles.module.scss @@ -0,0 +1,10 @@ +.keyInfoLoading { + margin-top: 8px; + padding-left: 16px; +} + +.keyTTL { + width: 86px; + min-width: 86px; + text-align: right; +} diff --git a/redisinsight/ui/src/pages/browser/components/key-row-type/KeyRowType.spec.tsx b/redisinsight/ui/src/pages/browser/components/key-row-type/KeyRowType.spec.tsx new file mode 100644 index 0000000000..170f56aea4 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-type/KeyRowType.spec.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' + +import { render } from 'uiSrc/utils/test-utils' +import { KeyTypes } from 'uiSrc/constants' +import KeyRowType, { Props } from './KeyRowType' + +const mockedProps = mock() +const loadingTestId = 'type-loading_' +const nameString = 'name' + +describe('KeyRowType', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should render Loading if no type', () => { + const { queryByTestId } = render() + + expect(queryByTestId(loadingTestId + nameString)).toBeInTheDocument() + }) + + it('should render Badge if type exists', () => { + const type = KeyTypes.Hash + const { queryByTestId } = render() + + expect(queryByTestId(loadingTestId + nameString)).not.toBeInTheDocument() + expect(queryByTestId(`badge-${type}_${nameString}`)).toBeInTheDocument() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/components/key-row-type/KeyRowType.tsx b/redisinsight/ui/src/pages/browser/components/key-row-type/KeyRowType.tsx new file mode 100644 index 0000000000..45d6e920aa --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-type/KeyRowType.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import cx from 'classnames' +import { + EuiLoadingContent, +} from '@elastic/eui' + +import { KeyTypes, ModulesKeyTypes } from 'uiSrc/constants' +import { GroupBadge } from 'uiSrc/components' +import styles from './styles.module.scss' + +export interface Props { + nameString: string + type: KeyTypes | ModulesKeyTypes +} + +const KeyRowType = (props: Props) => { + const { nameString, type } = props + + return ( + <> + {!type && ( + + )} + {!!type && ( +
+ +
+ )} + + ) +} + +export default KeyRowType diff --git a/redisinsight/ui/src/pages/browser/components/key-row-type/index.ts b/redisinsight/ui/src/pages/browser/components/key-row-type/index.ts new file mode 100644 index 0000000000..772282a678 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-type/index.ts @@ -0,0 +1,3 @@ +import KeyRowType from './KeyRowType' + +export default KeyRowType diff --git a/redisinsight/ui/src/pages/browser/components/key-row-type/styles.module.scss b/redisinsight/ui/src/pages/browser/components/key-row-type/styles.module.scss new file mode 100644 index 0000000000..01ef8ef796 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-type/styles.module.scss @@ -0,0 +1,11 @@ +.keyInfoLoading { + margin-top: 8px; + padding-left: 16px; +} + +.keyType { + padding-right: 16px; + padding-left: 12px; + width: 126px; + min-width: 126px; +} diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.spec.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.spec.tsx index 60435f70ab..b4c35023d2 100644 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.spec.tsx @@ -2,10 +2,15 @@ import { cloneDeep } from 'lodash' import React from 'react' import { cleanup, + fireEvent, mockedStore, render, } from 'uiSrc/utils/test-utils' import { KeysStoreData } from 'uiSrc/slices/interfaces/keys' +import { setBrowserTreeNodesOpen } from 'uiSrc/slices/app/context' +import { stringToBuffer } from 'uiSrc/utils' +import { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys' +import { KeyTypes } from 'uiSrc/constants' import KeyTree from './KeyTree' let store: typeof mockedStore @@ -48,31 +53,58 @@ const propsMock = { lastRefreshTime: 3 } as KeysStoreData, loading: false, + deleting: false, + commonFilterType: null, selectKey: jest.fn(), + loadMoreItems: jest.fn(), + onDelete: jest.fn(), + onAddKeyPanel: jest.fn(), + onBulkActionsPanel: jest.fn(), } +const leafRootFullName = 'test' +const folderFullName = 'car:' +const leaf1FullName = 'car:110' +const leaf2FullName = 'car:210' + const mockWebWorkerResult = [{ children: [{ children: [], - fullName: 'car:110:', - id: '0.snc1rc3zwgo', + fullName: leaf1FullName, + id: '0.0', + keyApproximate: 0.01, + keyCount: 1, + name: '110', + type: KeyTypes.String, + isLeaf: true, + nameBuffer: stringToBuffer(leaf1FullName), + }, { + children: [], + fullName: leaf2FullName, + id: '0.1', keyApproximate: 0.01, keyCount: 1, name: '110', + type: KeyTypes.Hash, + isLeaf: true, + nameBuffer: stringToBuffer(leaf2FullName), }], - fullName: 'car:', - id: '0.sz1ie1koqi8', + fullName: folderFullName, + id: '0', keyApproximate: 47.18, keyCount: 4718, name: 'car', }, { children: [], - fullName: 'test', - id: '0.snc1rc3zwg1o', + fullName: leafRootFullName, + id: '1', keyApproximate: 0.01, keyCount: 1, + type: KeyTypes.Stream, + isLeaf: true, name: 'test', + nameBuffer: stringToBuffer(leafRootFullName), }] jest.mock('uiSrc/services', () => ({ @@ -80,16 +112,58 @@ jest.mock('uiSrc/services', () => ({ useDisposableWebworker: () => ({ result: mockWebWorkerResult, run: jest.fn() }), })) +jest.mock('uiSrc/slices/browser/keys', () => ({ + ...jest.requireActual('uiSrc/slices/browser/keys'), + selectedKeyDataSelector: jest.fn().mockReturnValue(null), +})) + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + describe('KeyTree', () => { - it.only('Tree view panel should be in the document', () => { - const { container } = render() + it('should be rendered', () => { + expect(render()).toBeTruthy() + }) - expect(container.querySelector('[data-test-subj="tree-view-panel"]')).toBeInTheDocument() + it('"setBrowserTreeNodesOpen" to be called after click on folder', () => { + const onSelectedKeyMock = jest.fn() + const { getByTestId } = render() + + // set open state + fireEvent.click(getByTestId(`node-item_${folderFullName}`)) + + const expectedActions = [ + setBrowserTreeNodesOpen({ [folderFullName]: true }), + ] + + expect(store.getActions()).toEqual(expect.arrayContaining(expectedActions)) + }) + + it('"selectKey" to be called after click on leaf', async () => { + const onSelectedKeyMock = jest.fn() + const { getByTestId } = render() + + // open parent folder + fireEvent.click(getByTestId(`node-item_${folderFullName}`)) + + // click on the leaf + fireEvent.click(getByTestId(`node-item_${leaf2FullName}`)) + + expect(onSelectedKeyMock).toBeCalled() }) - it('Key list panel should be in the document', () => { - const { container } = render() + it('selected key from key list should be opened and selected in the tree', async () => { + const selectedKeyDataSelectorMock = jest.fn().mockReturnValue({ + name: stringToBuffer(leaf2FullName), + nameString: leaf2FullName, + }); + + (selectedKeyDataSelector as jest.Mock).mockImplementation(selectedKeyDataSelectorMock) + + const { getByTestId } = render() - expect(container.querySelector('[data-test-subj="key-list-panel"]')).toBeInTheDocument() + expect(getByTestId(`node-item_${leaf2FullName}`)).toBeInTheDocument() }) }) diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx index b289631390..9e9d2d10d7 100644 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx @@ -10,7 +10,7 @@ import { setBrowserTreeNodesOpen, } from 'uiSrc/slices/app/context' import { constructKeysToTree } from 'uiSrc/helpers' -import VirtualTree from 'uiSrc/components/virtual-tree' +import VirtualTree from 'uiSrc/pages/browser/components/virtual-tree' import TreeViewSVG from 'uiSrc/assets/img/icons/treeview.svg' import { KeysStoreData } from 'uiSrc/slices/interfaces/keys' import { Nullable, bufferToString } from 'uiSrc/utils' @@ -94,9 +94,7 @@ const KeyTree = forwardRef((props: Props, ref) => { // remove key name from parents parents.pop() - setTimeout(() => { - parents.forEach((parent) => handleStatusOpen(parent, true)) - }, 0) + parents.forEach((parent) => handleStatusOpen(parent, true)) } } diff --git a/redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.spec.tsx b/redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.spec.tsx index 9dd48c0813..de130e1c72 100644 --- a/redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.spec.tsx @@ -1,8 +1,11 @@ import React from 'react' -import { mock } from 'ts-mockito' +import { instance, mock } from 'ts-mockito' import { cloneDeep } from 'lodash' import { render, mockedStore, cleanup } from 'uiSrc/utils/test-utils' +import { keysSelector } from 'uiSrc/slices/browser/keys' +import { SearchMode } from 'uiSrc/slices/interfaces/keys' +import { redisearchSelector } from 'uiSrc/slices/browser/redisearch' import NoKeysMessage, { Props } from './NoKeysMessage' const mockedProps = mock() @@ -14,8 +17,160 @@ beforeEach(() => { store.clearActions() }) +jest.mock('uiSrc/slices/browser/keys', () => ({ + ...jest.requireActual('uiSrc/slices/browser/keys'), + keysSelector: jest.fn().mockReturnValue({ + searchMode: 'Pattern', + filter: null, + search: '', + viewType: 'Browser', + }), +})) + +jest.mock('uiSrc/slices/browser/redisearch', () => ({ + ...jest.requireActual('uiSrc/slices/browser/redisearch'), + redisearchSelector: jest.fn().mockReturnValue({ + search: '', + isSearched: false, + selectedIndex: null, + }), +})) + describe('NoKeysMessage', () => { it('should render', () => { expect(render()).toBeTruthy() }) + + describe('SearchMode = Pattern', () => { + it('NoKeysFound should be rendered if total=0', () => { + const { queryByTestId } = render() + expect(queryByTestId('no-result-found-msg')).toBeInTheDocument() + }) + + it('"scan-no-results-found" should be rendered if searched and scanned < total', () => { + const keysSelectorMock = jest.fn().mockReturnValue({ + isSearched: true, + searchMode: SearchMode.Pattern, + }) + const total = 100; + + (keysSelector as jest.Mock).mockImplementation(keysSelectorMock) + const { queryByTestId } = render( + + ) + + expect(queryByTestId('scan-no-results-found')).toBeInTheDocument() + }) + + it('"no-result-found" should be rendered if searched and scanned===total', () => { + const keysSelectorMock = jest.fn().mockReturnValue({ + isSearched: true, + searchMode: SearchMode.Pattern, + }) + const total = 100; + + (keysSelector as jest.Mock).mockImplementation(keysSelectorMock) + const { container } = render( + + ) + + expect(container.querySelector('[data-test-subj="no-result-found"]')).toBeInTheDocument() + }) + + it('"scan-no-results-found" should be rendered if filtered and scanned { + const keysSelectorMock = jest.fn().mockReturnValue({ + isFiltered: true, + searchMode: SearchMode.Pattern, + }) + const total = 100; + + (keysSelector as jest.Mock).mockImplementation(keysSelectorMock) + const { queryByTestId } = render( + + ) + + expect(queryByTestId('scan-no-results-found')).toBeInTheDocument() + }) + }) + + describe('SearchMode = RediSearch', () => { + it('"no-result-select-index" should be rendered if searched and scanned < total', () => { + const keysSelectorMock = jest.fn().mockReturnValue({ + isSearched: true, + searchMode: SearchMode.Redisearch, + }) + const redisearchSelectorMock = jest.fn().mockReturnValue({ + selectedIndex: null + }) + const total = 100; + + (keysSelector as jest.Mock).mockImplementation(keysSelectorMock); + (redisearchSelector as jest.Mock).mockImplementation(redisearchSelectorMock) + const { queryByTestId } = render( + + ) + + expect(queryByTestId('no-result-select-index')).toBeInTheDocument() + }) + it('"no-result-found-only" should be rendered total = 0', () => { + const keysSelectorMock = jest.fn().mockReturnValue({ + isSearched: true, + searchMode: SearchMode.Redisearch, + }) + const redisearchSelectorMock = jest.fn().mockReturnValue({ + selectedIndex: '123' + }) + const total = 0; + + (keysSelector as jest.Mock).mockImplementation(keysSelectorMock); + (redisearchSelector as jest.Mock).mockImplementation(redisearchSelectorMock) + const { queryByTestId } = render( + + ) + + expect(queryByTestId('no-result-found-only')).toBeInTheDocument() + }) + + it('"no-result-found-only" should be rendered if searched and scanned { + const keysSelectorMock = jest.fn().mockReturnValue({ + searchMode: SearchMode.Redisearch, + }) + const redisearchSelectorMock = jest.fn().mockReturnValue({ + isSearched: true, + selectedIndex: '123', + }) + const total = 100; + + (keysSelector as jest.Mock).mockImplementation(keysSelectorMock); + (redisearchSelector as jest.Mock).mockImplementation(redisearchSelectorMock) + const { queryByTestId } = render( + + ) + + expect(queryByTestId('no-result-found-only')).toBeInTheDocument() + }) + }) }) diff --git a/redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.tsx b/redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.tsx index 844966e77f..8b3aa1a83f 100644 --- a/redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.tsx +++ b/redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.tsx @@ -29,8 +29,7 @@ const NoKeysMessage = (props:Props) => { onBulkActionsPanel, } = props - const { selectedIndex } = useSelector(redisearchSelector) - const { isSearched: redisearchIsSearched } = useSelector(redisearchSelector) + const { selectedIndex, isSearched: redisearchIsSearched } = useSelector(redisearchSelector) const { isSearched: patternIsSearched, isFiltered, searchMode } = useSelector(keysSelector) if (searchMode === SearchMode.Redisearch) { diff --git a/redisinsight/ui/src/components/virtual-tree/VirtualTree.spec.tsx b/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.spec.tsx similarity index 100% rename from redisinsight/ui/src/components/virtual-tree/VirtualTree.spec.tsx rename to redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.spec.tsx diff --git a/redisinsight/ui/src/components/virtual-tree/VirtualTree.tsx b/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.tsx similarity index 98% rename from redisinsight/ui/src/components/virtual-tree/VirtualTree.tsx rename to redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.tsx index ce2dbcee55..79cf3efaa5 100644 --- a/redisinsight/ui/src/components/virtual-tree/VirtualTree.tsx +++ b/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.tsx @@ -68,6 +68,8 @@ const VirtualTree = (props: Props) => { onDeleteLeaf, } = props + // console.log({ statusOpen }) + const { theme } = useContext(ThemeContext) const [rerenderState, rerender] = useState({}) const controller = useRef>(null) @@ -160,12 +162,14 @@ const VirtualTree = (props: Props) => { const entries = Object.entries(elements.current) getMetadata(entries) + + elements.current = [] }, 100) - const getMetadataNode = (nameBuffer: any, path: string) => { + const getMetadataNode = useCallback((nameBuffer: any, path: string) => { elements.current[path] = nameBuffer getMetadataDebounced() - } + }, []) // This helper function constructs the object that will be sent back at the step // [2] during the treeWalker function work. Except for the mandatory `data` @@ -185,7 +189,6 @@ const VirtualTree = (props: Props) => { size: node.size, type: node.type, fullName: node.fullName, - shortName: node.shortName, nestingLevel, deleting, path: node.path, diff --git a/redisinsight/ui/src/components/virtual-tree/components/Node/Node.spec.tsx b/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.spec.tsx similarity index 58% rename from redisinsight/ui/src/components/virtual-tree/components/Node/Node.spec.tsx rename to redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.spec.tsx index 427ecc8f77..588cd1df4b 100644 --- a/redisinsight/ui/src/components/virtual-tree/components/Node/Node.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.spec.tsx @@ -2,19 +2,31 @@ import React from 'react' import { NodePublicState } from 'react-vtree/dist/es/Tree' import { instance, mock } from 'ts-mockito' import { render, screen } from 'uiSrc/utils/test-utils' -import KeyDarkSVG from 'uiSrc/assets/img/sidebar/browser_active.svg' +import { stringToBuffer } from 'uiSrc/utils' +import { KeyTypes } from 'uiSrc/constants' import Node from './Node' import { TreeData } from '../../interfaces' import { mockVirtualTreeResult } from '../../VirtualTree.spec' +const mockDataFullName = 'test' const mockedProps = mock>() const mockedPropsData = mock() const mockedData: TreeData = { ...instance(mockedPropsData), nestingLevel: 3, - leafIcon: KeyDarkSVG + isLeaf: true, + path: '0.0.5.6', + fullName: mockDataFullName, + nameString: mockDataFullName, + nameBuffer: stringToBuffer(mockDataFullName), +} + +const mockedDataWithMetadata = { + ...mockedData, + type: KeyTypes.Hash, + ttl: 123, + size: 123, } -const mockDataFullName = 'test' jest.mock('uiSrc/services', () => ({ ...jest.requireActual('uiSrc/services'), @@ -39,29 +51,45 @@ describe('Node', () => { expect(container.querySelector(`[data-test-subj="node-folder-icon_${mockDataFullName}"`)).toBeInTheDocument() }) - it('should render leaf icon for Leaf properly', () => { + it('"setItems", "updateStatusSelected", "mockGetMetadata" should be called after click on Leaf', () => { + const mockUpdateStatusSelected = jest.fn() + const mockUpdateStatusOpen = jest.fn() + const mockSetOpen = jest.fn() + const mockGetMetadata = jest.fn() + const mockData: TreeData = { ...mockedData, - isLeaf: true, - fullName: mockDataFullName + updateStatusSelected: mockUpdateStatusSelected, + updateStatusOpen: mockUpdateStatusOpen, + getMetadata: mockGetMetadata, } - const { container } = render() + render() - expect(container.querySelector(`[data-test-subj="leaf-icon_${mockDataFullName}"`)).toBeInTheDocument() + screen.getByTestId(`node-item_${mockDataFullName}`).click() + + expect(mockUpdateStatusSelected).toBeCalledWith(mockData.nameBuffer) + expect(mockUpdateStatusOpen).toBeCalledWith(mockDataFullName, true) + expect(mockGetMetadata).toBeCalledWith(mockData.nameBuffer, mockData.path) + expect(mockSetOpen).not.toBeCalled() }) - it('"setItems", "updateStatusSelected" should be called after click on Leaf', () => { + it('"mockGetMetadata" not be call if size and ttl exists', () => { const mockUpdateStatusSelected = jest.fn() const mockUpdateStatusOpen = jest.fn() const mockSetOpen = jest.fn() + const mockGetMetadata = jest.fn() const mockData: TreeData = { - ...mockedData, - isLeaf: true, - fullName: mockDataFullName, + ...mockedDataWithMetadata, updateStatusSelected: mockUpdateStatusSelected, updateStatusOpen: mockUpdateStatusOpen, + getMetadata: mockGetMetadata, } render( { screen.getByTestId(`node-item_${mockDataFullName}`).click() - expect(mockUpdateStatusSelected).toBeCalledWith(mockDataFullName) + expect(mockUpdateStatusSelected).toBeCalledWith(mockData.nameBuffer) expect(mockUpdateStatusOpen).toBeCalledWith(mockDataFullName, true) + expect(mockGetMetadata).not.toBeCalled() expect(mockSetOpen).not.toBeCalled() }) + it.only('name, ttl and size should be rendered', () => { + const { getByTestId } = render() + + expect(getByTestId(`node-item_${mockDataFullName}`)).toBeInTheDocument() + expect(getByTestId(`badge-${mockedDataWithMetadata.type}_${mockDataFullName}`)).toBeInTheDocument() + expect(getByTestId(`ttl-${mockDataFullName}`)).toBeInTheDocument() + expect(getByTestId(`size-${mockDataFullName}`)).toBeInTheDocument() + }) + it('"updateStatusOpen", "setOpen" should be called after click on Node', () => { const mockUpdateStatusSelected = jest.fn() const mockUpdateStatusOpen = jest.fn() diff --git a/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.tsx b/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.tsx new file mode 100644 index 0000000000..a0ae1b7278 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.tsx @@ -0,0 +1,198 @@ +import React, { useEffect, useState } from 'react' +import { NodePublicState } from 'react-vtree/dist/es/Tree' +import cx from 'classnames' +import { + EuiIcon, + EuiToolTip, + keys as ElasticKeys, +} from '@elastic/eui' + +import { + Maybe, +} from 'uiSrc/utils' +import { KeyTypes, ModulesKeyTypes } from 'uiSrc/constants' +import KeyRowTTL from 'uiSrc/pages/browser/components/key-row-ttl' +import KeyRowSize from 'uiSrc/pages/browser/components/key-row-size' +import KeyRowName from 'uiSrc/pages/browser/components/key-row-name' +import KeyRowType from 'uiSrc/pages/browser/components/key-row-type' +import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' +import { TreeData } from '../../interfaces' +import styles from './styles.module.scss' + +const MAX_NESTING_LEVEL = 20 + +// Node component receives all the data we created in the `treeWalker` + +// internal openness state (`isOpen`), function to change internal openness +// `style` parameter that should be added to the root div. +const Node = ({ + data, + isOpen, + index, + style, + setOpen, +}: NodePublicState) => { + const { + id: nodeId, + isLeaf, + keyCount, + nestingLevel, + fullName, + nameBuffer, + path, + type, + ttl, + size, + deleting, + nameString, + keyApproximate, + isSelected, + getMetadata, + onDelete, + onDeleteClicked, + updateStatusOpen, + updateStatusSelected, + } = data + + const [deletePopoverId, setDeletePopoverId] = useState>(undefined) + + useEffect(() => { + if (!isLeaf || !nameBuffer) { + return + } + if (!size || !ttl) { + getMetadata?.(nameBuffer, path) + } + }, []) + + useEffect(() => { + if (isSelected && nameBuffer) { + updateStatusSelected?.(nameBuffer) + } + }, [isSelected]) + + const handleClick = () => { + if (isLeaf && !isSelected) { + updateStatusSelected?.(nameBuffer) + } + + updateStatusOpen?.(fullName, !isOpen) + !isLeaf && setOpen(!isOpen) + } + + const handleKeyDown = ({ key }: React.KeyboardEvent) => { + if (key === ElasticKeys.SPACE) { + handleClick() + } + } + + const handleDelete = (nameBuffer: RedisResponseBuffer) => { + onDelete(nameBuffer) + setDeletePopoverId(undefined) + } + + const handleDeletePopoverOpen = (index: Maybe, type: KeyTypes | ModulesKeyTypes) => { + if (index !== deletePopoverId) { + onDeleteClicked(type) + } + setDeletePopoverId(index !== deletePopoverId ? index : undefined) + } + + const Folder = () => ( + <> +
+ + + + {nameString} + +
+
+
+ {keyApproximate ? `${keyApproximate < 1 ? '<1' : Math.round(keyApproximate)}%` : '' } +
+
{keyCount ?? ''}
+
+ + ) + + const Leaf = () => ( + <> + + + + + + ) + + const Node = ( +
{}} + data-testid={`node-item_${fullName}`} + > + {!isLeaf && } + {isLeaf && } +
+ ) + + const tooltipContent = ( + <> + {`${fullName}*`} +
+ {`${keyCount} key(s) (${Math.round(keyApproximate * 100) / 100}%)`} + + ) + + return ( +
MAX_NESTING_LEVEL ? MAX_NESTING_LEVEL : nestingLevel) * 8, + }} + className={cx( + styles.nodeContainer, { + [styles.nodeSelected]: isSelected && isLeaf, + [styles.nodeRowEven]: index % 2 === 0, + } + )} + > + {isLeaf && Node} + {!isLeaf && ( + + {Node} + + )} +
+ ) +} + +export default Node diff --git a/redisinsight/ui/src/components/virtual-tree/components/Node/index.ts b/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/index.ts similarity index 100% rename from redisinsight/ui/src/components/virtual-tree/components/Node/index.ts rename to redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/index.ts diff --git a/redisinsight/ui/src/components/virtual-tree/components/Node/styles.module.scss b/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/styles.module.scss similarity index 88% rename from redisinsight/ui/src/components/virtual-tree/components/Node/styles.module.scss rename to redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/styles.module.scss index 1af538d04f..653a14ad0b 100644 --- a/redisinsight/ui/src/components/virtual-tree/components/Node/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/styles.module.scss @@ -37,25 +37,25 @@ color: var(--euiColorFullShade) !important; } - .moveOnHover { + :global(.moveOnHoverKey) { transition: transform ease 0.3s; &.hide { transform: translateX(-8px); } } - .showOnHover { - display: none; + :global(.showOnHoverKey) { + display: none !important; &.show { display: flex !important; } } &:hover { - .moveOnHover { + :global(.moveOnHoverKey) { transform: translateX(-8px); } - .showOnHover { - display: flex; + :global(.showOnHoverKey) { + display: flex !important; } } } @@ -105,11 +105,16 @@ min-width: 126px; } -.keyName { +.keyName, +.nodeName { flex-grow: 1; position: relative; overflow: hidden; text-overflow: ellipsis; + + :global(.euiTextColor) { + max-width: 100%; + } } .keyTTL, diff --git a/redisinsight/ui/src/components/virtual-tree/index.ts b/redisinsight/ui/src/pages/browser/components/virtual-tree/index.ts similarity index 100% rename from redisinsight/ui/src/components/virtual-tree/index.ts rename to redisinsight/ui/src/pages/browser/components/virtual-tree/index.ts diff --git a/redisinsight/ui/src/components/virtual-tree/interfaces.ts b/redisinsight/ui/src/pages/browser/components/virtual-tree/interfaces.ts similarity index 97% rename from redisinsight/ui/src/components/virtual-tree/interfaces.ts rename to redisinsight/ui/src/pages/browser/components/virtual-tree/interfaces.ts index 717cd34fe4..8d34865532 100644 --- a/redisinsight/ui/src/components/virtual-tree/interfaces.ts +++ b/redisinsight/ui/src/pages/browser/components/virtual-tree/interfaces.ts @@ -25,7 +25,6 @@ export interface NodeMetaData { keyCount: number, name: string, fullName: string, - shortName: string, updateStatusSelected: (fullName: string, keys: any) => void, updateStatusOpen: (name: string, value: boolean) => void, leafIcon: string, @@ -43,7 +42,6 @@ export interface TreeData extends FixedSizeNodeData { keyCount: number keyApproximate: number fullName: string - shortName: string leafIcon: string type: KeyTypes | ModulesKeyTypes ttl: number diff --git a/redisinsight/ui/src/components/virtual-tree/styles.module.scss b/redisinsight/ui/src/pages/browser/components/virtual-tree/styles.module.scss similarity index 100% rename from redisinsight/ui/src/components/virtual-tree/styles.module.scss rename to redisinsight/ui/src/pages/browser/components/virtual-tree/styles.module.scss diff --git a/yarn.lock b/yarn.lock index 80a342afd6..b261b82fa8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4585,10 +4585,10 @@ ci-info@^2.0.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== -ci-info@^3.2.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" - integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== +ci-info@^3.2.0, ci-info@^3.7.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" @@ -7091,6 +7091,13 @@ find-versions@^4.0.0: dependencies: semver-regex "^3.1.2" +find-yarn-workspace-root@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd" + integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ== + dependencies: + micromatch "^4.0.2" + flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -7516,7 +7523,7 @@ got@^11.7.0, got@^11.8.5: p-cancelable "^2.0.0" responselike "^2.0.0" -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -8615,7 +8622,7 @@ is-word-character@^1.0.0: resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.4.tgz#ce0e73216f98599060592f62ff31354ddbeb0230" integrity sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA== -is-wsl@^2.2.0: +is-wsl@^2.1.1, is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== @@ -9313,6 +9320,13 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +json-stable-stringify@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.2.tgz#e06f23128e0bbe342dc996ed5a19e28b57b580e0" + integrity sha512-eunSSaEnxV12z+Z73y/j5N37/In40GK4GmsSy+tEHJMxknvqnA7/djeYtAgW0GsWHUfg+847WJjKaEylk2y09g== + dependencies: + jsonify "^0.0.1" + json-stringify-safe@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -9346,6 +9360,11 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +jsonify@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" + integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== + jsonpath@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/jsonpath/-/jsonpath-1.1.1.tgz#0ca1ed8fb65bb3309248cc9d5466d12d5b0b9901" @@ -9412,6 +9431,13 @@ kind-of@^6.0.2, kind-of@^6.0.3: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +klaw-sync@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c" + integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ== + dependencies: + graceful-fs "^4.1.11" + kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" @@ -11022,6 +11048,14 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +open@^7.4.2: + version "7.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" + integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== + dependencies: + is-docker "^2.0.0" + is-wsl "^2.1.1" + open@^8.0.9: version "8.4.2" resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" @@ -11259,6 +11293,27 @@ pascal-case@^3.1.2: no-case "^3.0.4" tslib "^2.0.3" +patch-package@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-8.0.0.tgz#d191e2f1b6e06a4624a0116bcb88edd6714ede61" + integrity sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA== + dependencies: + "@yarnpkg/lockfile" "^1.1.0" + chalk "^4.1.2" + ci-info "^3.7.0" + cross-spawn "^7.0.3" + find-yarn-workspace-root "^2.0.0" + fs-extra "^9.0.0" + json-stable-stringify "^1.0.2" + klaw-sync "^6.0.0" + minimist "^1.2.6" + open "^7.4.2" + rimraf "^2.6.3" + semver "^7.5.3" + slash "^2.0.0" + tmp "^0.0.33" + yaml "^2.2.2" + path-browserify@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" @@ -11652,6 +11707,11 @@ postcss@^8.2.15, postcss@^8.2.9: picocolors "^1.0.0" source-map-js "^1.0.2" +postinstall-postinstall@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz#4f7f77441ef539d1512c40bd04c71b06a4704ca3" + integrity sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ== + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -12679,6 +12739,13 @@ rfdc@^1.3.0: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== +rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -13047,6 +13114,11 @@ skip-postinstall@^1.0.0: resolved "https://registry.yarnpkg.com/skip-postinstall/-/skip-postinstall-1.0.0.tgz#939d49b09ddae9816f089c0b892a8e4bb7bc7747" integrity sha512-IUVEmm4v7Ubzrp9JDG15oTzMB+abJdHcduXMRzBlHnHRrmpQ/QoPtYCRaorP+abAULTGEh87gPPyyMK5H1X1Dg== +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -15013,6 +15085,11 @@ yaml@^1.10.0, yaml@^1.10.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +yaml@^2.2.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.3.tgz#01f6d18ef036446340007db8e016810e5d64aad9" + integrity sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ== + yargs-parser@20.x, yargs-parser@^20.2.2, yargs-parser@^20.2.3: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" From 8265f1eafa8b925308eae10367e9aaa0d742f46d Mon Sep 17 00:00:00 2001 From: zalenskiSofteq Date: Fri, 20 Oct 2023 19:30:39 +0200 Subject: [PATCH 03/12] #RI-4828 - Re-work the Tree view --- package.json | 2 +- redisinsight/ui/src/constants/storage.ts | 1 + .../KeyTreeSettings/styles.module.scss | 4 +- .../components/virtual-tree/VirtualTree.tsx | 2 - redisinsight/ui/src/slices/app/context.ts | 1 + .../ui/src/slices/tests/app/context.spec.ts | 28 ++++++++- .../ui/src/slices/tests/browser/keys.spec.ts | 58 +++++++++++++++++++ 7 files changed, 89 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 5c26df7596..7fd621a913 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "start:web": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./configs/webpack.config.web.dev.ts", "start:web:public": "cross-env PUBLIC_DEV=true NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./configs/webpack.config.web.dev.ts", "test": "jest ./redisinsight/ui -w 1", - "test:watch": "jest ./redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.spec.tsx --watch -w 1", + "test:watch": "jest ./redisinsight/ui/src/slices/tests/app/context.spec.ts --watch -w 1", "test:cov": "jest ./redisinsight/ui --silent --coverage --no-cache --forceExit -w 3", "test:cov:unit": "jest ./redisinsight/ui --group=-component --coverage -w 1", "test:cov:component": "jest ./redisinsight/ui --group=component --coverage -w 1", diff --git a/redisinsight/ui/src/constants/storage.ts b/redisinsight/ui/src/constants/storage.ts index be27828c17..4627732ac4 100644 --- a/redisinsight/ui/src/constants/storage.ts +++ b/redisinsight/ui/src/constants/storage.ts @@ -13,6 +13,7 @@ enum BrowserStorageItem { wbInputHistory = 'wbInputHistory', isEnablementAreaMinimized = 'isEnablementAreaMinimized', treeViewDelimiter = 'treeViewDelimiter', + treeViewSort = 'treeViewSort', autoRefreshRate = 'autoRefreshRate', bulkActionDeleteId = 'bulkActionDeleteId', dbConfig = 'dbConfig_', diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/styles.module.scss b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/styles.module.scss index 08bfec6e5c..9118c31517 100644 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/styles.module.scss @@ -54,7 +54,7 @@ } .title { - font-size: 14px; + font-size: 14px; font-weight: 500; } @@ -81,4 +81,4 @@ button { margin-left: 8px; } -} \ No newline at end of file +} diff --git a/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.tsx b/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.tsx index 79cf3efaa5..d46139ae34 100644 --- a/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.tsx +++ b/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.tsx @@ -68,8 +68,6 @@ const VirtualTree = (props: Props) => { onDeleteLeaf, } = props - // console.log({ statusOpen }) - const { theme } = useContext(ThemeContext) const [rerenderState, rerender] = useState({}) const controller = useRef>(null) diff --git a/redisinsight/ui/src/slices/app/context.ts b/redisinsight/ui/src/slices/app/context.ts index 46d7ee2daf..689788918a 100644 --- a/redisinsight/ui/src/slices/app/context.ts +++ b/redisinsight/ui/src/slices/app/context.ts @@ -95,6 +95,7 @@ const appContextSlice = createSlice({ }, setDbConfig: (state, { payload }) => { state.dbConfig.treeViewDelimiter = payload?.treeViewDelimiter ?? DEFAULT_DELIMITER + state.dbConfig.treeViewSort = payload?.treeViewSort ?? DEFAULT_TREE_SORTING state.dbConfig.slowLogDurationUnit = payload?.slowLogDurationUnit ?? DEFAULT_SLOWLOG_DURATION_UNIT state.dbConfig.showHiddenRecommendations = payload?.showHiddenRecommendations ?? DEFAULT_SHOW_HIDDEN_RECOMMENDATIONS diff --git a/redisinsight/ui/src/slices/tests/app/context.spec.ts b/redisinsight/ui/src/slices/tests/app/context.spec.ts index 75e05b99f6..6f4e6d723e 100644 --- a/redisinsight/ui/src/slices/tests/app/context.spec.ts +++ b/redisinsight/ui/src/slices/tests/app/context.spec.ts @@ -1,6 +1,6 @@ import { cloneDeep } from 'lodash' -import { DEFAULT_DELIMITER, KeyTypes } from 'uiSrc/constants' -import { getTreeLeafField, stringToBuffer } from 'uiSrc/utils' +import { KeyTypes, SortOrder } from 'uiSrc/constants' +import { stringToBuffer } from 'uiSrc/utils' import { cleanup, @@ -16,6 +16,7 @@ import reducer, { setBrowserSelectedKey, setBrowserPatternScrollPosition, setBrowserPanelSizes, + setBrowserTreeSort, setWorkbenchScript, setWorkbenchVerticalPanelSizes, setLastPageContext, @@ -504,6 +505,7 @@ describe('slices', () => { const data = { slowLogDurationUnit: 'msec', treeViewDelimiter: ':-', + treeViewSort: SortOrder.DESC, showHiddenRecommendations: true, } @@ -568,6 +570,28 @@ describe('slices', () => { }) }) + describe('setBrowserTreeSort', () => { + it('should properly set browser tree sorting', () => { + // Arrange + const sorting = SortOrder.DESC + + const state = { + ...initialState.dbConfig, + treeViewSort: sorting, + } + + // Act + const nextState = reducer(initialState, setBrowserTreeSort(sorting)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + app: { context: nextState }, + }) + + expect(appContextDbConfig(rootState)).toEqual(state) + }) + }) + describe('setRecommendationsShowHidden', () => { it('should properly set is show hidden live recommendations', () => { // Arrange diff --git a/redisinsight/ui/src/slices/tests/browser/keys.spec.ts b/redisinsight/ui/src/slices/tests/browser/keys.spec.ts index de9b08a1da..1faf107ea8 100644 --- a/redisinsight/ui/src/slices/tests/browser/keys.spec.ts +++ b/redisinsight/ui/src/slices/tests/browser/keys.spec.ts @@ -52,6 +52,7 @@ import reducer, { fetchKeyInfo, fetchKeys, fetchKeysMetadata, + fetchKeysMetadataTree, fetchMoreKeys, fetchPatternHistoryAction, fetchSearchHistoryAction, @@ -1650,6 +1651,63 @@ describe('keys slice', () => { }) }) + describe('fetchKeysMetadataTree', () => { + it('success to fetch keys metadata', async () => { + // Arrange + const data = [ + { + name: stringToBuffer('key1'), + type: 'hash', + ttl: -1, + size: 100, + path: 0, + length: 100, + }, + { + name: stringToBuffer('key2'), + type: 'hash', + ttl: -1, + size: 150, + path: 1, + length: 100, + }, + { + name: stringToBuffer('key3'), + type: 'hash', + ttl: -1, + size: 110, + path: 2, + length: 100, + }, + ] + const responsePayload = { data, status: 200 } + + const apiServiceMock = jest.fn().mockResolvedValue(responsePayload) + const onSuccessMock = jest.fn() + apiService.post = apiServiceMock + const controller = new AbortController() + + // Act + await store.dispatch( + fetchKeysMetadataTree( + data.map(({ name }, i) => ([i, name])), + null, + controller.signal, + onSuccessMock, + ) + ) + + // Assert + expect(apiServiceMock).toBeCalledWith( + '/databases//keys/get-metadata', + { keys: data.map(({ name }, i) => (name)), type: undefined }, + { params: { encoding: 'buffer' }, signal: controller.signal }, + ) + + expect(onSuccessMock).toBeCalledWith(data) + }) + }) + describe('addKeyIntoList', () => { it('updateKeyList should be called', async () => { // Act From bd09d61244b056b41dbdc7cce69cb55ad51cfceb Mon Sep 17 00:00:00 2001 From: zalenskiSofteq Date: Tue, 24 Oct 2023 13:26:48 +0200 Subject: [PATCH 04/12] #RI-4828 - fix pr comments --- jest.config.cjs | 4 ---- package.json | 2 +- .../components/filter-key-type/FilterKeyType.tsx | 4 +++- .../key-tree/KeyTreeSettings/KeyTreeSettings.tsx | 1 + .../components/no-keys-message/NoKeysMessage.tsx | 2 +- .../components/search-key-list/SearchKeyList.tsx | 4 +++- .../browser/components/virtual-tree/VirtualTree.tsx | 10 +++++----- 7 files changed, 14 insertions(+), 13 deletions(-) diff --git a/jest.config.cjs b/jest.config.cjs index 24b0c9f37d..66ae5b8524 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -39,7 +39,6 @@ module.exports = { 'json', ], testEnvironment: 'jest-environment-jsdom', - // type: 'module', transformIgnorePatterns: [ 'node_modules/(?!(monaco-editor|react-monaco-editor)/)', ], @@ -61,8 +60,5 @@ module.exports = { functions: 72, lines: 80, }, - // './redisinsight/ui/src/slices/**/*.ts': { - // statements: 90, - // }, }, } diff --git a/package.json b/package.json index 7fd621a913..0769e63f96 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "start:web": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./configs/webpack.config.web.dev.ts", "start:web:public": "cross-env PUBLIC_DEV=true NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./configs/webpack.config.web.dev.ts", "test": "jest ./redisinsight/ui -w 1", - "test:watch": "jest ./redisinsight/ui/src/slices/tests/app/context.spec.ts --watch -w 1", + "test:watch": "jest ./redisinsight/ui --watch -w 1", "test:cov": "jest ./redisinsight/ui --silent --coverage --no-cache --forceExit -w 3", "test:cov:unit": "jest ./redisinsight/ui --group=-component --coverage -w 1", "test:cov:component": "jest ./redisinsight/ui --group=component --coverage -w 1", diff --git a/redisinsight/ui/src/pages/browser/components/filter-key-type/FilterKeyType.tsx b/redisinsight/ui/src/pages/browser/components/filter-key-type/FilterKeyType.tsx index f75df5ae4c..43585a4b64 100644 --- a/redisinsight/ui/src/pages/browser/components/filter-key-type/FilterKeyType.tsx +++ b/redisinsight/ui/src/pages/browser/components/filter-key-type/FilterKeyType.tsx @@ -75,7 +75,9 @@ const FilterKeyType = () => { setTypeSelected(value) setIsSelectOpen(false) dispatch(setFilter(value === ALL_KEY_TYPES_VALUE ? null : value)) - viewType === KeyViewType.Tree && dispatch(resetBrowserTree()) + if (viewType === KeyViewType.Tree) { + dispatch(resetBrowserTree()) + } dispatch( fetchKeys( { diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.tsx index 8febd940db..95d401b0c0 100644 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.tsx @@ -38,6 +38,7 @@ const KeyTreeSettings = ({ loading }: Props) => { const dispatch = useDispatch() useEffect(() => { + setSorting(treeViewSort) }, [treeViewSort]) useEffect(() => { diff --git a/redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.tsx b/redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.tsx index 8b3aa1a83f..a0f1762608 100644 --- a/redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.tsx +++ b/redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.tsx @@ -21,7 +21,7 @@ export interface Props { onBulkActionsPanel: (value: boolean) => void } -const NoKeysMessage = (props:Props) => { +const NoKeysMessage = (props: Props) => { const { total, scanned, diff --git a/redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.tsx b/redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.tsx index 01824f0aa8..fbd3ff302d 100644 --- a/redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.tsx +++ b/redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.tsx @@ -67,7 +67,9 @@ const SearchKeyList = () => { dispatch(setSearchMatch(match, searchMode)) - viewType === KeyViewType.Tree && dispatch(resetBrowserTree()) + if (viewType === KeyViewType.Tree) { + dispatch(resetBrowserTree()) + } dispatch(fetchKeys( { diff --git a/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.tsx b/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.tsx index d46139ae34..54cd1795b5 100644 --- a/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.tsx +++ b/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.tsx @@ -44,7 +44,7 @@ export interface Props { } interface OpenedNodes { - [key: string]: boolean; + [key: string]: boolean } export const KEYS = 'keys' @@ -81,7 +81,7 @@ const VirtualTree = (props: Props) => { useEffect(() => () => { nodes.current = [] - elements.current = [] + elements.current = {} }, []) // receive result from the "runWebworker" @@ -90,7 +90,7 @@ const VirtualTree = (props: Props) => { return } - elements.current = [] + elements.current = {} nodes.current = result rerender({}) setConstructingTree?.(false) @@ -99,7 +99,7 @@ const VirtualTree = (props: Props) => { useEffect(() => { if (!items?.length) { nodes.current = [] - elements.current = [] + elements.current = {} rerender({}) runWebworker?.({ items: [], delimiter, sorting }) return @@ -161,7 +161,7 @@ const VirtualTree = (props: Props) => { getMetadata(entries) - elements.current = [] + elements.current = {} }, 100) const getMetadataNode = useCallback((nameBuffer: any, path: string) => { From 02450eb4743d9c1f552a1a10f89771ccef10f6df Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Tue, 31 Oct 2023 14:28:15 +0100 Subject: [PATCH 05/12] updates for existing tree view tests --- .../KeyTreeSettings/KeyTreeSettings.tsx | 1 + tests/e2e/common-actions/browser-actions.ts | 52 +++++++- tests/e2e/helpers/keys.ts | 19 +-- tests/e2e/pageObjects/browser-page.ts | 116 ++---------------- .../pageObjects/components/browser/index.ts | 4 +- .../components/browser/tree-view.ts | 83 +++++++++++++ .../critical-path/browser/bulk-upload.e2e.ts | 4 +- .../tree-view/tree-view-improvements.e2e.ts | 97 ++++++++------- .../critical-path/browser/bulk-upload.e2e.ts | 4 +- .../web/critical-path/browser/context.e2e.ts | 17 +-- .../browser/search-capabilities.e2e.ts | 20 ++- .../database-overview/database-index.e2e.ts | 20 +-- .../memory-efficiency.e2e.ts | 6 +- .../critical-path/tree-view/delimiter.e2e.ts | 19 +-- .../tree-view/tree-view-improvements.e2e.ts | 116 +++++++++--------- .../critical-path/tree-view/tree-view.e2e.ts | 29 +---- .../web/regression/browser/filtering.e2e.ts | 4 +- .../web/regression/tree-view/tree-view.e2e.ts | 28 +++-- .../workbench/import-tutorials.e2e.ts | 4 +- 19 files changed, 327 insertions(+), 316 deletions(-) create mode 100644 tests/e2e/pageObjects/components/browser/tree-view.ts diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.tsx index 95d401b0c0..acc3011a46 100644 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.tsx @@ -151,6 +151,7 @@ const KeyTreeSettings = ({ loading }: Props) => { Cancel diff --git a/tests/e2e/common-actions/browser-actions.ts b/tests/e2e/common-actions/browser-actions.ts index 4eb63aacc5..a0afa741ae 100644 --- a/tests/e2e/common-actions/browser-actions.ts +++ b/tests/e2e/common-actions/browser-actions.ts @@ -1,4 +1,4 @@ -import {Selector, t} from 'testcafe'; +import { Selector, t } from 'testcafe'; import { BrowserPage } from '../pageObjects'; const browserPage = new BrowserPage(); @@ -29,6 +29,7 @@ export class BrowserActions { } } } + /** * Verify tooltip contains text * @param expectedText Expected link that is compared with actual @@ -39,17 +40,62 @@ export class BrowserActions { ? await t.expect(browserPage.tooltip.textContent).contains(expectedText, `"${expectedText}" Text is incorrect in tooltip`) : await t.expect(browserPage.tooltip.textContent).notContains(expectedText, `Tooltip still contains text "${expectedText}"`); } + /** * Verify that the new key is displayed at the top of the list of keys and opened and pre-selected in List view - * */ + * @param keyName Key name + */ async verifyKeyDisplayedTopAndOpened(keyName: string): Promise { await t.expect(Selector('[aria-rowindex="1"]').withText(keyName).visible).ok(`element with ${keyName} is not visible in the top of list`); await t.expect(browserPage.keyNameFormDetails.withText(keyName).visible).ok(`element with ${keyName} is not opened`); } + /** * Verify that the new key is not displayed at the top of the list of keys and opened and pre-selected in List view - * */ + * @param keyName Key name + */ async verifyKeyIsNotDisplayedTop(keyName: string): Promise { await t.expect(Selector('[aria-rowindex="1"]').withText(keyName).exists).notOk(`element with ${keyName} is not visible in the top of list`); } + + /** + * Check tree view structure + * @folders name of folders for tree view build + * @delimiter string with delimiter value + * @commonKeyFolder flag if not patterned keys will be displayed + */ + async checkTreeViewFoldersStructure(folders: string[][], delimiter: string, commonKeyFolder: boolean): Promise { + // Verify that all keys that are not inside of tree view doesn't contain delimiter + if (commonKeyFolder) { + const notPatternedKeys = Selector('[data-testid^="badge"]').parent('[data-testid^="node-item_"]'); + const notPatternedKeysNumber = await notPatternedKeys.count; + for (let i = 0; i < notPatternedKeysNumber; i++) { + await t.expect(notPatternedKeys.nth(i).withText(delimiter).exists).notOk('Not contained delimiter keys'); + } + } + // Verify that every level of tree view is clickable + const foldersNumber = folders.length; + for (let i = 0; i < foldersNumber; i++) { + const innerFoldersNumber = folders[i].length; + const array: string[] = []; + for (let j = 0; j < innerFoldersNumber; j++) { + if (j === 0) { + const folderSelector = `[data-testid="node-item_${folders[i][j]}${delimiter}"]`; + array.push(folderSelector); + await t.click(Selector(folderSelector)); + } + else { + const lastSelector = array[array.length - 1].substring(0, array[array.length - 1].length - 2); + const folderSelector = `${lastSelector}${folders[i][j]}${delimiter}"]`; + array.push(folderSelector); + await t.click(Selector(folderSelector)); + } + } + // Verify that the last folder level contains required keys + const foundKeyName = `${folders[i].join(delimiter)}`; + await t + .expect(Selector(`[data-testid*="key-${foundKeyName}"]`).exists).ok('Specific key not found') + .click(array[0]); + } + } } diff --git a/tests/e2e/helpers/keys.ts b/tests/e2e/helpers/keys.ts index 517d45a6f2..8e1fa94393 100644 --- a/tests/e2e/helpers/keys.ts +++ b/tests/e2e/helpers/keys.ts @@ -249,21 +249,13 @@ export async function deleteAllKeysFromDB(host: string, port: string): Promise { +export async function verifyKeysDisplayingInTheList(keyNames: string[], isDisplayed: boolean): Promise { for (const keyName of keyNames) { - await t.expect(browserPage.getKeySelectorByName(keyName).exists).ok(`The key ${keyName} not found`); - } -} - -/** -* Verifying if the Keys are not in the List of keys -* @param keyNames The names of the keys -*/ - -export async function verifyKeysNotDisplayedInTheList(keyNames: string[]): Promise { - for (const keyName of keyNames) { - await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).notOk(`The key ${keyName} found`); + isDisplayed + ? await t.expect(browserPage.getKeySelectorByName(keyName).exists).ok(`The key ${keyName} not found`) + : await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).notOk(`The key ${keyName} found`); } } @@ -271,7 +263,6 @@ export async function verifyKeysNotDisplayedInTheList(keyNames: string[]): Promi * Verify search/filter value * @param value The value in search/filter input */ - export async function verifySearchFilterValue(value: string): Promise { await t.expect(browserPage.filterByPatterSearchInput.withAttribute('value', value).exists).ok(`Filter per key name ${value} is not applied/correct`); } diff --git a/tests/e2e/pageObjects/browser-page.ts b/tests/e2e/pageObjects/browser-page.ts index 141888d2c0..558bdd890a 100644 --- a/tests/e2e/pageObjects/browser-page.ts +++ b/tests/e2e/pageObjects/browser-page.ts @@ -1,10 +1,11 @@ import { t, Selector } from 'testcafe'; import { Common } from '../helpers/common'; import { InstancePage } from './instance-page'; -import { BulkActions } from './components/browser'; +import { BulkActions, TreeView } from './components/browser'; export class BrowserPage extends InstancePage { BulkActions = new BulkActions(); + TreeView = new TreeView(); //CSS Selectors cssSelectorGrid = '[aria-label="grid"]'; @@ -71,13 +72,9 @@ export class BrowserPage extends InstancePage { databaseInfoIcon = Selector('[data-testid=db-info-icon]'); treeViewButton = Selector('[data-testid=view-type-list-btn]'); browserViewButton = Selector('[data-testid=view-type-browser-btn]'); - treeViewSeparator = Selector('[data-testid=tree-view-delimiter-btn]'); searchButton = Selector('[data-testid=search-btn]'); clearFilterButton = Selector('[data-testid=reset-filter-btn]'); clearSelectionButton = Selector('[data-testid=clear-selection-btn]'); - treeViewDelimiterButton = Selector('[data-testid=tree-view-delimiter-btn]'); - treeViewDelimiterValueSave = Selector('[data-testid=apply-btn]'); - treeViewDelimiterValueCancel = Selector('[data-testid=cancel-btn]'); fullScreenModeButton = Selector('[data-testid=toggle-full-screen]'); closeRightPanel = Selector('[data-testid=close-right-panel-btn]'); addNewStreamEntry = Selector('[data-testid=add-key-value-items-btn]'); @@ -177,7 +174,6 @@ export class BrowserPage extends InstancePage { jsonKeyInput = Selector('[data-testid=json-key]'); jsonValueInput = Selector('[data-testid=json-value]'); countInput = Selector('[data-testid=count-input]'); - treeViewDelimiterInput = Selector('[data-testid=tree-view-delimiter-input]'); streamEntryId = Selector('[data-testid=entryId]'); streamField = Selector('[data-testid=field-name]'); streamValue = Selector('[data-testid=field-value]'); @@ -211,28 +207,19 @@ export class BrowserPage extends InstancePage { jsonKeyValue = Selector('[data-testid=json-data]'); jsonError = Selector('[data-testid=edit-json-error]'); tooltip = Selector('[role=tooltip]'); - noResultsFound = Selector('[data-test-subj=no-result-found]'); + noResultsFound = Selector('[data-testid=no-result-found-only]'); searchAdvices = Selector('[data-test-subj=search-advices]'); keysNumberOfResults = Selector('[data-testid=keys-number-of-results]'); keysTotalNumber = Selector('[data-testid=keys-total]'); overviewConnectedClients = Selector('[data-test-subj=overview-connected-clients]'); overviewCommandsSec = Selector('[data-test-subj=overview-commands-sec]'); overviewCpu = Selector('[data-test-subj=overview-cpu]'); - treeViewArea = Selector('[data-test-subj=tree-view-panel]'); scannedValue = Selector('[data-testid=keys-number-of-scanned]'); - treeViewKeysNumber = Selector('[data-testid^=count_]'); - treeViewPercentage = Selector('[data-testid^=percentage_]'); - treeViewFolders = Selector('[data-test-subj^=node-arrow-icon_]'); totalKeysNumber = Selector('[data-testid=keys-total]'); databaseInfoToolTip = Selector('[data-testid=db-info-tooltip]'); - treeViewDeviceFolder = Selector('[data-testid^=node-item_device] div'); - treeViewDeviceKyesCount = Selector('[data-testid^=count_device] span'); ttlValueInKeysTable = Selector('[data-testid^=ttl-]'); stringKeyValue = Selector('.key-details-body pre'); keyDetailsBadge = Selector('.key-details-header .euiBadge__text'); - treeViewKeysItem = Selector('[data-testid*="keys:keys:"]'); - treeViewNotPatternedKeys = Selector('[data-testid*="node-item_keys"]'); - treeViewNodeArrowIcon = Selector('[data-test-subj^=node-arrow-icon_]'); modulesTypeDetails = Selector('[data-testid=modules-type-details]'); filteringLabel = Selector('[data-testid^=badge-]'); keysSummary = Selector('[data-testid=keys-summary]'); @@ -590,6 +577,7 @@ export class BrowserPage extends InstancePage { /** * Delete Key By name after Hovering + * @param keyName The name of the key */ async deleteKeyByNameFromList(keyName: string): Promise { await this.searchByKeyName(keyName); @@ -766,7 +754,10 @@ export class BrowserPage extends InstancePage { .click(this.saveMemberButton); } - //Open key details + /** + * Open key details with search + * @param keyName The name of the key + */ async openKeyDetails(keyName: string): Promise { await this.searchByKeyName(keyName); await t.click(this.keyNameInTheList); @@ -878,66 +869,6 @@ export class BrowserPage extends InstancePage { await t.typeText(this.jsonValueInput, jsonStructure, { replace: true, paste: true }); await t.click(this.applyEditButton); } - /** - * Check tree view structure - * @folders name of folders for tree view build - * @delimiter string with delimiter value - * @commonKeyFolder flag if not patterned keys will be displayed - */ - async checkTreeViewFoldersStructure(folders: string[][], delimiter: string, commonKeyFolder: boolean): Promise { - // Verify that all keys that are not inside of tree view doesn't contain delimiter - if (commonKeyFolder) { - await t - .expect(this.treeViewNotPatternedKeys.exists).ok('Folder with not patterned keys') - .click(this.treeViewNotPatternedKeys); - const notPatternedKeys = Selector('[data-test-subj=key-list-panel]').find(this.cssSelectorKey); - const notPatternedKeysNumber = await notPatternedKeys.count; - for (let i = 0; i < notPatternedKeysNumber; i++) { - await t.expect(notPatternedKeys.nth(i).withText(delimiter).exists).notOk('Not contained delimiter keys'); - } - } - // Verify that every level of tree view is clickable - const foldersNumber = folders.length; - for (let i = 0; i < foldersNumber; i++) { - const innerFoldersNumber = folders[i].length; - const array: string[] = []; - for (let j = 0; j < innerFoldersNumber; j++) { - if (j === 0) { - const folderSelector = `[data-testid="node-item_${folders[i][j]}${delimiter}"]`; - array.push(folderSelector); - await t.click(Selector(folderSelector)); - } - else { - const lastSelector = array[array.length - 1].substring(0, array[array.length - 1].length - 2); - const folderSelector = `${lastSelector}${folders[i][j]}${delimiter}"]`; - array.push(folderSelector); - await t.click(Selector(folderSelector)); - } - } - // Verify that the last folder level contains required keys - const lastSelector = array[array.length - 1].substring(0, array[array.length - 1].length - 2); - const folderSelector = `${lastSelector}keys${delimiter}keys${delimiter}"]`; - await t.click(Selector(folderSelector)); - const foundKeyName = `${folders[i].join(delimiter)}`; - await t - .expect(Selector(`[data-testid*="key-${foundKeyName}"]`).exists).ok('Specific key not found') - .click(array[0]); - } - } - - /** - * Change delimiter value - * @delimiter string with delimiter value - */ - async changeDelimiterInTreeView(delimiter: string): Promise { - // Open delimiter popup - await t.click(this.treeViewDelimiterButton); - // Apply new value to the field - await t.typeText(this.treeViewDelimiterInput, delimiter, { replace: true, paste: true }); - // Click on save button - await t.click(this.treeViewDelimiterValueSave); - await t.expect(this.treeViewDelimiterButton.withExactText(delimiter).exists).ok('Delimiter is not changed'); - } //Delete entry from Stream key async deleteStreamEntry(): Promise { @@ -1019,33 +950,6 @@ export class BrowserPage extends InstancePage { .click(option); } - /** - * Get text from first tree element - */ - async getTextFromNthTreeElement(number: number): Promise { - return (await Selector('[role="treeitem"]').nth(number).find('div').textContent).replace(/\s/g, ''); - } - - /** - * Open tree folder with multiple level - * @param names folder names with sequence of subfolder - */ - async openTreeFolders(names: string[]): Promise { - let base = `node-item_${names[0]}:`; - await t.click(Selector(`[data-testid="${base}"]`)); - if (names.length > 1) { - for (let i = 1; i < names.length; i++) { - base = `${base }${names[i]}:`; - await t.click(Selector(`[data-testid="${base}"]`)); - } - } - await t.click(Selector(`[data-testid="${base}keys:keys:"]`)); - - await t.expect( - Selector(`[data-testid="${base}keys:keys:"]`).visible) - .ok('Folder is not selected'); - } - /** * Verify that database has no keys */ @@ -1061,6 +965,10 @@ export class BrowserPage extends InstancePage { await t.click(this.clearFilterButton); } + /** + * Open Guide link by name + * @param guide The guide name + */ async clickGuideLinksByName(guide: string): Promise { const linkGuide = Selector(`[data-testid="guide-button-${guide}"]`); await t.click(linkGuide); diff --git a/tests/e2e/pageObjects/components/browser/index.ts b/tests/e2e/pageObjects/components/browser/index.ts index 8a8463c193..79bba1bb76 100644 --- a/tests/e2e/pageObjects/components/browser/index.ts +++ b/tests/e2e/pageObjects/components/browser/index.ts @@ -1,5 +1,7 @@ import { BulkActions } from './bulk-actions'; +import { TreeView } from './tree-view'; export { - BulkActions + BulkActions, + TreeView, }; diff --git a/tests/e2e/pageObjects/components/browser/tree-view.ts b/tests/e2e/pageObjects/components/browser/tree-view.ts new file mode 100644 index 0000000000..0472b24f6b --- /dev/null +++ b/tests/e2e/pageObjects/components/browser/tree-view.ts @@ -0,0 +1,83 @@ +import { Selector, t } from 'testcafe'; + +export class TreeView { + //------------------------------------------------------------------------------------------- + //DECLARATION OF SELECTORS + //*Declare all elements/components of the relevant page. + //*Target any element/component via data-id, if possible! + //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.). + //------------------------------------------------------------------------------------------- + //BUTTONS + treeViewSettingsBtn = Selector('[data-testid=tree-view-settings-btn]'); + treeViewDelimiterValueSave = Selector('[data-testid=tree-view-apply-btn]'); + treeViewDelimiterValueCancel = Selector('[data-testid=tree-view-cancel-btn]'); + // TEXT ELEMENTS + treeViewKeysNumber = Selector('[data-testid^=count_]'); + treeViewDeviceFolder = Selector('[data-testid^=node-item_device] div'); + //INPUTS + treeViewDelimiterInput = Selector('[data-testid=tree-view-delimiter-input]'); + + + /** + * Get folder selector by folder name + * @param folderName The name of the folder + */ + getFolderSelectorByName(folderName: string): Selector { + return Selector(`[data-testid^="node-item_${folderName}"]`); + } + + /** + * Get folder counter selector by folder name + * @param folderName The name of the folder + */ + getFolderCountSelectorByName(folderName: string): Selector { + return Selector(`[data-testid^="count_${folderName}"]`); + } + + /** + * Verifying if the Keys are in the List of keys + * @param keyNames The names of the keys + * @param isDisplayed True if keys should be displayed + */ + async verifyFolderDisplayingInTheList(folderName: string, isDisplayed: boolean): Promise { + isDisplayed + ? await t.expect(this.getFolderSelectorByName(folderName).exists).ok(`The folder ${folderName} not found`) + : await t.expect(this.getFolderSelectorByName(folderName).exists).notOk(`The folder ${folderName} found`); + } + + /** + * Change delimiter value + * @delimiter string with delimiter value + */ + async changeDelimiterInTreeView(delimiter: string): Promise { + // Open delimiter popup + await t.click(this.treeViewSettingsBtn); + // Apply new value to the field + await t.typeText(this.treeViewDelimiterInput, delimiter, { replace: true, paste: true }); + // Click on save button + await t.click(this.treeViewDelimiterValueSave); + } + + /** + * Get text from tree element by number + * @param number The number of tree folder + */ + async getTextFromNthTreeElement(number: number): Promise { + return (await Selector('[role="treeitem"]').nth(number).find('div').textContent).replace(/\s/g, ''); + } + + /** + * Open tree folder with multiple level + * @param names folder names with sequence of subfolder + */ + async openTreeFolders(names: string[]): Promise { + let base = `node-item_${names[0]}:`; + await t.click(Selector(`[data-testid="${base}"]`)); + if (names.length > 1) { + for (let i = 1; i < names.length; i++) { + base = `${base }${names[i]}:`; + await t.click(Selector(`[data-testid="${base}"]`)); + } + } + } +} diff --git a/tests/e2e/tests/electron/critical-path/browser/bulk-upload.e2e.ts b/tests/e2e/tests/electron/critical-path/browser/bulk-upload.e2e.ts index a86c1d2706..fb6d8202b9 100644 --- a/tests/e2e/tests/electron/critical-path/browser/bulk-upload.e2e.ts +++ b/tests/e2e/tests/electron/critical-path/browser/bulk-upload.e2e.ts @@ -5,7 +5,7 @@ import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneRedisearch } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; -import { deleteAllKeysFromDB, verifyKeysDisplayedInTheList } from '../../../../helpers/keys'; +import { deleteAllKeysFromDB, verifyKeysDisplayingInTheList } from '../../../../helpers/keys'; const browserPage = new BrowserPage(); const databaseHelper = new DatabaseHelper(); @@ -53,7 +53,7 @@ test('Verify bulk upload of different text docs formats', async t => { await browserPage.BulkActions.uploadFileInBulk(filePathes.allKeysFile); await verifyCompletedResultText(allKeysResults); await browserPage.searchByKeyName('*key1'); - await verifyKeysDisplayedInTheList(keyNames); + await verifyKeysDisplayingInTheList(keyNames, true); // Verify that Upload button disabled after starting new upload await t.click(browserPage.BulkActions.bulkActionStartNewButton); diff --git a/tests/e2e/tests/electron/critical-path/tree-view/tree-view-improvements.e2e.ts b/tests/e2e/tests/electron/critical-path/tree-view/tree-view-improvements.e2e.ts index f3db07e435..4a5126f300 100644 --- a/tests/e2e/tests/electron/critical-path/tree-view/tree-view-improvements.e2e.ts +++ b/tests/e2e/tests/electron/critical-path/tree-view/tree-view-improvements.e2e.ts @@ -5,7 +5,7 @@ import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { KeyTypesTexts, rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; -import { verifyKeysDisplayedInTheList, verifyKeysNotDisplayedInTheList } from '../../../../helpers/keys'; +import { verifyKeysDisplayingInTheList } from '../../../../helpers/keys'; import { APIKeyRequests } from '../../../../helpers/api/api-keys'; const browserPage = new BrowserPage(); @@ -27,12 +27,11 @@ test await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .after(async() => { - await t.click(browserPage.patternModeBtn); - await browserPage.deleteKeysByNames(keyNames); + await browserPage.Cli.sendCommandInCli('flushdb'); await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Tree view preselected folder', async t => { - keyName1 = Common.generateWord(10); // used to create index name - keyName2 = Common.generateWord(10); // used to create index name + keyName1 = Common.generateWord(10); + keyName2 = Common.generateWord(10); keyNameSingle = Common.generateWord(10); keyNames = [`${keyName1}:1`, `${keyName1}:2`, `${keyName2}:1`, `${keyName2}:2`, keyNameSingle]; @@ -41,7 +40,7 @@ test `HSET ${keyNames[0]} field value`, `HSET ${keyNames[1]} field value`, `HSET ${keyNames[2]} field value`, - `HSET ${keyNames[3]} field value`, + `SADD ${keyNames[3]} value`, `SADD ${keyNames[4]} value` ]; @@ -49,17 +48,17 @@ test await browserPage.Cli.sendCommandsInCli(commands); await t.click(browserPage.treeViewButton); // The folder without any patterns selected and the list of keys is displayed when there is a folder without any patterns - await verifyKeysDisplayedInTheList([keyNameSingle]); + await verifyKeysDisplayingInTheList([keyNameSingle], true); - await browserPage.openTreeFolders([await browserPage.getTextFromNthTreeElement(1)]); + await browserPage.TreeView.openTreeFolders([await browserPage.TreeView.getTextFromNthTreeElement(1)]); await browserPage.selectFilterGroupType(KeyTypesTexts.Set); // The folder without any namespaces is selected (if exists) when folder does not exist after search/filter - await verifyKeysDisplayedInTheList([keyNameSingle]); + await verifyKeysDisplayingInTheList([keyNameSingle], true); await browserPage.setAllKeyType(); // The folder without any patterns selected and the list of keys is displayed when there is a folder without any patterns - await verifyKeysDisplayedInTheList([keyNameSingle]); - await verifyKeysNotDisplayedInTheList([`${keyNames[0]}:1`, `${keyNames[2]}:2`]); + await verifyKeysDisplayingInTheList([keyNameSingle], true); + await verifyKeysDisplayingInTheList([`${keyNames[0]}:1`, `${keyNames[2]}:2`], false); // switch between browser view and tree view await t.click(browserPage.browserViewButton) @@ -67,51 +66,51 @@ test await browserPage.deleteKeyByName(keyNames[4]); await t.click(browserPage.clearFilterButton); // get first folder name - const firstTreeItemText = await browserPage.getTextFromNthTreeElement(0); - const firstTreeItemKeys = Selector(`[data-testid="node-item_${firstTreeItemText}:keys:keys:"]`); // keys after node item opened - // The first folder with namespaces is expanded and selected when there is no folder without any patterns - await t.expect(firstTreeItemKeys.visible) - .ok('First folder is not expanded'); - await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); + const firstTreeItemText = await browserPage.TreeView.getTextFromNthTreeElement(0); + // All folders with namespaces are collapsed when there is no folder without any patterns + await verifyKeysDisplayingInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`], false); const commands1 = [ - `HSET ${keyNames[4]} field value` + `SADD ${keyNames[4]} value` ]; await browserPage.Cli.sendCommandsInCli(commands1); await t.click(browserPage.refreshKeysButton); - // Refreshed Tree view preselected folder - await t.expect(firstTreeItemKeys.visible) - .ok('Folder is not selected'); - await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); + // Folders are collapsed after refresh + await verifyKeysDisplayingInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`], false); + await verifyKeysDisplayingInTheList([keyNameSingle], true); await browserPage.selectFilterGroupType(KeyTypesTexts.Hash); - await t.expect(firstTreeItemKeys.visible).ok('Folder is not selected after searching with HASH'); - // Filtered Tree view preselected folder - await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); + // Only folders according to key type filter are displayed + await verifyKeysDisplayingInTheList([keyNameSingle], false); + await browserPage.TreeView.verifyFolderDisplayingInTheList(firstTreeItemText, true); - await browserPage.searchByKeyName('*'); - // Search capability Filtered Tree view preselected folder - await t.expect(firstTreeItemKeys.visible).ok('Folder is not selected'); - await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); + await browserPage.searchByKeyName(`${keyName1}*`); + // Only folders according to filter by key names are displayed + await verifyKeysDisplayingInTheList([keyNameSingle], false); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, false); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); await t.click(browserPage.clearFilterButton); - // Filtered Tree view preselected folder - await t.expect(firstTreeItemKeys.visible).ok('Folder is not selected'); - await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); + // All folders are displayed and collapsed after cleared filter + await verifyKeysDisplayingInTheList([keyNameSingle], true); + await verifyKeysDisplayingInTheList([keyName1], false); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, true); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); await browserPage.selectFilterGroupType(KeyTypesTexts.Stream); - // Filtered Tree view preselected folder - await t.expect(browserPage.keyListTable.textContent).contains('No results found.', 'Key is not found message not displayed'); + // Verify that No results found message is displayed in case of invalid filtering + await t.expect(browserPage.noResultsFound.textContent).contains('No results found.', 'Key is not found message not displayed'); await browserPage.setAllKeyType(); // clear stream from filter - // Filtered Tree view preselected folder - await t.expect(browserPage.keyListTable.textContent).notContains('No results found.', 'Key is not found message still displayed'); - await t.expect( - firstTreeItemKeys.exists) - .notOk('First folder is expanded'); + // Verify that no results found message not displayed after clearing filter + await t.expect(browserPage.noResultsFound.exists).notOk('Key is not found message still displayed'); + // All folders are displayed and collapsed after cleared filter + await verifyKeysDisplayingInTheList([keyNameSingle], true); + await verifyKeysDisplayingInTheList([keyName1], false); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, true); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); }); - test .before(async() => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); @@ -140,7 +139,7 @@ test await browserPage.selectIndexByName(index); await t.click(browserPage.treeViewButton); await t.click(Selector(`[data-testid="${`node-item_${folders[0]}:`}"]`)); // close folder - await browserPage.openTreeFolders(folders); + await browserPage.TreeView.openTreeFolders(folders); await t.click(browserPage.refreshKeysButton); // Refreshed Tree view preselected folder for index based search await t.expect( @@ -174,24 +173,24 @@ test await browserPage.Cli.sendCommandsInCli(commands); await t.click(browserPage.treeViewButton); // The folder without any patterns selected and the list of keys is displayed when there is a folder without any patterns - await verifyKeysDisplayedInTheList([keyNameSingle]); + await verifyKeysDisplayingInTheList([keyNameSingle], true); - await browserPage.openTreeFolders([keyName1]); // Type: hash - await browserPage.openTreeFolders([keyName2]); // Type: list + await browserPage.TreeView.openTreeFolders([keyName1]); // Type: hash + await browserPage.TreeView.openTreeFolders([keyName2]); // Type: list await browserPage.selectFilterGroupType(KeyTypesTexts.Hash); // The first folder with namespaces is expanded and selected when folder and folder without any namespaces does not exist after search/filter - await verifyKeysDisplayedInTheList([keyNames[0], keyNames[1]]); + await verifyKeysDisplayingInTheList([keyNames[0], keyNames[1]], true); await browserPage.setAllKeyType(); await browserPage.Cli.sendCommandsInCli([`DEL ${keyNames[0]}`]); await t.click(browserPage.refreshKeysButton); // refresh keys // The previously selected folder is preselected when key does not exist after keys refresh - await verifyKeysDisplayedInTheList([keyNames[1]]); - await verifyKeysNotDisplayedInTheList([keyNames[0], keyNames[2], keyNames[3], keyNames[4]]); + await verifyKeysDisplayingInTheList([keyNames[1]], true); + await verifyKeysDisplayingInTheList([keyNames[0], keyNames[2], keyNames[3], keyNames[4]], false); await browserPage.searchByKeyName('*'); await t.click(browserPage.refreshKeysButton); // Search capability Refreshed Tree view preselected folder - await verifyKeysDisplayedInTheList([keyNames[1]]); - await verifyKeysNotDisplayedInTheList([keyNames[0], keyNames[2], keyNames[3], keyNames[4]]); + await verifyKeysDisplayingInTheList([keyNames[1]], true); + await verifyKeysDisplayingInTheList([keyNames[0], keyNames[2], keyNames[3], keyNames[4]], false); }); diff --git a/tests/e2e/tests/web/critical-path/browser/bulk-upload.e2e.ts b/tests/e2e/tests/web/critical-path/browser/bulk-upload.e2e.ts index 1509482f92..b188a5f811 100644 --- a/tests/e2e/tests/web/critical-path/browser/bulk-upload.e2e.ts +++ b/tests/e2e/tests/web/critical-path/browser/bulk-upload.e2e.ts @@ -5,7 +5,7 @@ import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneRedisearch } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; -import { deleteAllKeysFromDB, verifyKeysDisplayedInTheList } from '../../../../helpers/keys'; +import { deleteAllKeysFromDB, verifyKeysDisplayingInTheList } from '../../../../helpers/keys'; const browserPage = new BrowserPage(); const databaseHelper = new DatabaseHelper(); @@ -53,7 +53,7 @@ test('Verify bulk upload of different text docs formats', async t => { await browserPage.BulkActions.uploadFileInBulk(filePathes.allKeysFile); await verifyCompletedResultText(allKeysResults); await browserPage.searchByKeyName('*key1'); - await verifyKeysDisplayedInTheList(keyNames); + await verifyKeysDisplayingInTheList(keyNames, true); // Verify that Upload button disabled after starting new upload await t.click(browserPage.BulkActions.bulkActionStartNewButton); diff --git a/tests/e2e/tests/web/critical-path/browser/context.e2e.ts b/tests/e2e/tests/web/critical-path/browser/context.e2e.ts index 9125b4a017..98be742867 100644 --- a/tests/e2e/tests/web/critical-path/browser/context.e2e.ts +++ b/tests/e2e/tests/web/critical-path/browser/context.e2e.ts @@ -100,12 +100,15 @@ test const scrollY = 1000; await t.scroll(browserPage.cssSelectorGrid, 0, scrollY); - const keysCount = await browserPage.virtualTableContainer.find(browserPage.cssVirtualTableRow).count; - const targetKey = browserPage.virtualTableContainer.find(browserPage.cssVirtualTableRow).nth(Math.floor(keysCount / 2)); + const virtualizedTableKeyIndex = browserPage.virtualTableContainer.find(browserPage.cssVirtualTableRow).nth(10); + const targetKeyIndex = await virtualizedTableKeyIndex.getAttribute('aria-rowindex'); + const targetKey = browserPage.virtualTableContainer.find(`[aria-rowindex="${targetKeyIndex}"`); const targetKeyName = await targetKey.find(browserPage.cssSelectorKey).innerText; + // Open key details await t.click(targetKey); - await t.expect(await targetKey.getAttribute('class')).contains('table-row-selected', 'Not correct key selected in key list'); + // Verify that key selected + await t.expect(targetKey.getAttribute('class')).contains('table-row-selected', 'Not correct key selected in key list'); await t.click(myRedisDatabasePage.NavigationPanel.settingsButton); // Return back to Browser and check key details selected @@ -113,7 +116,7 @@ test // Check Keys details saved await t.expect(browserPage.keyNameFormDetails.innerText).eql(targetKeyName, 'Key details is not saved as context'); // Check Key selected in Key List - await t.expect(await targetKey.getAttribute('class')).contains('table-row-selected', 'Not correct key selected in key list'); + await t.expect(targetKey.getAttribute('class')).contains('table-row-selected', 'Not correct key selected in key list'); }); test .after(async() => { @@ -132,13 +135,13 @@ test await t.click(browserPage.Cli.cliCollapseButton); await t.click(browserPage.refreshKeysButton); - const keyList = await browserPage.keyListTable; - const keyListSGrid = await keyList.find(browserPage.cssSelectorGrid); + const keyList = browserPage.keyListTable; + const keyListSGrid = keyList.find(browserPage.cssSelectorGrid); // Scroll key list await t.scroll(keyListSGrid, 0, scrollY); // Find any key from list that is visible - const renderedRows = await keyList.find(browserPage.cssSelectorRows); + const renderedRows = keyList.find(browserPage.cssSelectorRows); const renderedRowsCount = await renderedRows.count; const randomKey = renderedRows.nth(Math.floor((Math.random() * renderedRowsCount))); const randomKeyName = await randomKey.find(browserPage.cssSelectorKey).textContent; diff --git a/tests/e2e/tests/web/critical-path/browser/search-capabilities.e2e.ts b/tests/e2e/tests/web/critical-path/browser/search-capabilities.e2e.ts index ac7d0b952d..75e76fceae 100644 --- a/tests/e2e/tests/web/critical-path/browser/search-capabilities.e2e.ts +++ b/tests/e2e/tests/web/critical-path/browser/search-capabilities.e2e.ts @@ -10,7 +10,7 @@ import { import { rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; -import { verifyKeysDisplayedInTheList, verifyKeysNotDisplayedInTheList } from '../../../../helpers/keys'; +import { verifyKeysDisplayingInTheList } from '../../../../helpers/keys'; import { APIKeyRequests } from '../../../../helpers/api/api-keys'; const browserPage = new BrowserPage(); @@ -81,7 +81,7 @@ test // Verify that user can search by index in Browser view await browserPage.selectIndexByName(indexName); - await verifyKeysDisplayedInTheList(keyNames); + await verifyKeysDisplayingInTheList(keyNames, true); await t.expect(browserPage.getKeySelectorByName(keyName).exists).notOk('Key without index displayed after search'); // Verify that user can search by index plus key value await browserPage.searchByKeyName('Hall School'); @@ -107,15 +107,15 @@ test // Verify that user can search by index in Tree view await t.click(browserPage.treeViewButton); // Change delimiter - await browserPage.changeDelimiterInTreeView('-'); + await browserPage.TreeView.changeDelimiterInTreeView('-'); await browserPage.selectIndexByName(indexName); - await verifyKeysDisplayedInTheList(keyNames); + await verifyKeysDisplayingInTheList(keyNames, true); await t.expect(browserPage.getKeySelectorByName(keyName).exists).notOk('Key without index displayed after search'); // Verify that user see the database scanned when he switch to Pattern search mode await t.click(browserPage.patternModeBtn); await t.click(browserPage.browserViewButton); - await verifyKeysDisplayedInTheList(keyNames); + await verifyKeysDisplayingInTheList(keyNames, true); await t.expect(browserPage.getKeySelectorByName(keyName).exists).ok('Database not scanned after returning to Pattern search mode'); }); test @@ -273,9 +273,7 @@ test await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); })('Verify that indexed keys from previous DB are NOT displayed when user connects to another DB', async t => { - /* - Link to ticket: https://redislabs.atlassian.net/browse/RI-3863 - */ + // Link to ticket: https://redislabs.atlassian.net/browse/RI-3863 // key names to validate in the standalone database keyNames = [`${keyNameSimpleDb}:1`, `${keyNameSimpleDb}:2`, `${keyNameSimpleDb}:3`, `${keyNameSimpleDb}:4`, `${keyNameSimpleDb}:5`]; @@ -308,14 +306,14 @@ test await t.click(browserPage.treeViewButton); // switch to tree view await t.click(browserPage.redisearchModeBtn); // click redisearch button await browserPage.selectIndexByName(indexNameSimpleDb); // select pre-created index in the standalone database - await browserPage.changeDelimiterInTreeView('-'); // change delimiter in tree view to be able to verify keys easily + await browserPage.TreeView.changeDelimiterInTreeView('-'); // change delimiter in tree view to be able to verify keys easily - await verifyKeysDisplayedInTheList(keyNames); // verify created keys are visible + await verifyKeysDisplayingInTheList(keyNames, true); // verify created keys are visible await t.click(browserPage.OverviewPanel.myRedisDBLink); // go back to database selection page await myRedisDatabasePage.clickOnDBByName(bigDbName); // click database name from ossStandaloneBigConfig.databaseName - await verifyKeysNotDisplayedInTheList(keyNames); // Verify that standandalone database keys are NOT visible + await verifyKeysDisplayingInTheList(keyNames, false); // Verify that standandalone database keys are NOT visible await t.expect(Selector('span').withText('Select Index').exists).ok('Index is still selected'); }); diff --git a/tests/e2e/tests/web/critical-path/database-overview/database-index.e2e.ts b/tests/e2e/tests/web/critical-path/database-overview/database-index.e2e.ts index 033cb5a237..10cd1d4f2d 100644 --- a/tests/e2e/tests/web/critical-path/database-overview/database-index.e2e.ts +++ b/tests/e2e/tests/web/critical-path/database-overview/database-index.e2e.ts @@ -9,7 +9,7 @@ import { } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; -import { verifyKeysDisplayedInTheList, verifyKeysNotDisplayedInTheList, verifySearchFilterValue } from '../../../../helpers/keys'; +import { verifyKeysDisplayingInTheList, verifySearchFilterValue } from '../../../../helpers/keys'; const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); @@ -70,8 +70,8 @@ test('Switching between indexed databases', async t => { await browserPage.addHashKey(keyNameForSearchInLogicalDb); // Verify that data changed for indexed db on Tree view await t.click(browserPage.treeViewButton); - await verifyKeysDisplayedInTheList([keyNameForSearchInLogicalDb]); - await verifyKeysNotDisplayedInTheList(keyNames); + await verifyKeysDisplayingInTheList([keyNameForSearchInLogicalDb], true); + await verifyKeysDisplayingInTheList(keyNames, false); // Filter by Hash keys and search by key name await browserPage.selectFilterGroupType(KeyTypesTexts.Hash); @@ -81,25 +81,25 @@ test('Switching between indexed databases', async t => { // Verify that search/filter saved after switching index in Browser await verifySearchFilterValue(keyNameForSearchInLogicalDb); - await verifyKeysNotDisplayedInTheList([keyNameForSearchInLogicalDb]); + await verifyKeysDisplayingInTheList([keyNameForSearchInLogicalDb], false); await t.click(browserPage.browserViewButton); // Change index to logical db await browserPage.OverviewPanel.changeDbIndex(1); await verifySearchFilterValue(keyNameForSearchInLogicalDb); - await verifyKeysDisplayedInTheList([keyNameForSearchInLogicalDb]); + await verifyKeysDisplayingInTheList([keyNameForSearchInLogicalDb], true); // Return to default database and open search capability await browserPage.OverviewPanel.changeDbIndex(0); await t.click(browserPage.redisearchModeBtn); await browserPage.selectIndexByName(indexName); - await verifyKeysDisplayedInTheList(keyNames); + await verifyKeysDisplayingInTheList(keyNames, true); // Change index to logical db await browserPage.OverviewPanel.changeDbIndex(1); // Search by value and return to default database await browserPage.searchByKeyName('Hall School'); await browserPage.OverviewPanel.changeDbIndex(0); // Verify that data changed for indexed db on Search capability page - await verifyKeysDisplayedInTheList([keyNames[0]]); + await verifyKeysDisplayingInTheList([keyNames[0]], true); // Change index to logical db await browserPage.OverviewPanel.changeDbIndex(1); // Verify that search/filter saved after switching index in Search capability @@ -116,14 +116,14 @@ test('Switching between indexed databases', async t => { // Clear filter await t.click(browserPage.clearFilterButton); // Verify that data changed for indexed db on Workbench page (on Search capability page) - await verifyKeysDisplayedInTheList([logicalDbKey]); + await verifyKeysDisplayingInTheList([logicalDbKey], true); await t.click(browserPage.patternModeBtn); // Clear filter await t.click(browserPage.clearFilterButton); // Verify that data changed for indexed db on Workbench page - await verifyKeysDisplayedInTheList([keyNameForSearchInLogicalDb, logicalDbKey]); + await verifyKeysDisplayingInTheList([keyNameForSearchInLogicalDb, logicalDbKey], true); await browserPage.OverviewPanel.changeDbIndex(0); - await verifyKeysNotDisplayedInTheList([logicalDbKey]); + await verifyKeysDisplayingInTheList([logicalDbKey], false); // Go to Analysis Tools page and create new report await t.click(myRedisDatabasePage.NavigationPanel.analysisPageButton); diff --git a/tests/e2e/tests/web/critical-path/memory-efficiency/memory-efficiency.e2e.ts b/tests/e2e/tests/web/critical-path/memory-efficiency/memory-efficiency.e2e.ts index ad75b106d5..fc49f6025a 100644 --- a/tests/e2e/tests/web/critical-path/memory-efficiency/memory-efficiency.e2e.ts +++ b/tests/e2e/tests/web/critical-path/memory-efficiency/memory-efficiency.e2e.ts @@ -113,7 +113,7 @@ test .click(browserPage.treeViewButton) .click(browserPage.clearFilterButton); // Change delimiter - await browserPage.changeDelimiterInTreeView('-'); + await browserPage.TreeView.changeDelimiterInTreeView('-'); // Go to Analysis Tools page await t.click(myRedisDatabasePage.NavigationPanel.analysisPageButton); // Create new report @@ -125,7 +125,7 @@ test // No namespaces message with link await t.click(myRedisDatabasePage.NavigationPanel.browserButton); // Change delimiter to delimiter with no keys - await browserPage.changeDelimiterInTreeView('+'); + await browserPage.TreeView.changeDelimiterInTreeView('+'); // Go to Analysis Tools page and create report await t .click(myRedisDatabasePage.NavigationPanel.analysisPageButton) @@ -135,7 +135,7 @@ test 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'); + await t.expect(browserPage.TreeView.treeViewSettingsBtn.exists).ok('Tree view not opened'); }); test .before(async t => { diff --git a/tests/e2e/tests/web/critical-path/tree-view/delimiter.e2e.ts b/tests/e2e/tests/web/critical-path/tree-view/delimiter.e2e.ts index 1a800a1ba0..e96d37957f 100644 --- a/tests/e2e/tests/web/critical-path/tree-view/delimiter.e2e.ts +++ b/tests/e2e/tests/web/critical-path/tree-view/delimiter.e2e.ts @@ -3,8 +3,10 @@ import { commonUrl, ossStandaloneBigConfig } from '../../../../helpers/conf'; import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; +import { BrowserActions } from '../../../../common-actions/browser-actions'; const browserPage = new BrowserPage(); +const browserActions = new BrowserActions(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); @@ -20,19 +22,20 @@ fixture `Delimiter tests` test('Verify that user can see that input is not saved when the Cancel button is clicked', async t => { // Switch to tree view await t.click(browserPage.treeViewButton); + await t.click(browserPage.TreeView.treeViewSettingsBtn); // Check the default delimiter value - await t.expect(browserPage.treeViewDelimiterButton.withExactText(':').exists).ok('Default delimiter not applied'); - // Open delimiter popup - await t.click(browserPage.treeViewDelimiterButton); + await t.expect(browserPage.TreeView.treeViewDelimiterInput.value).eql(':', 'Default delimiter not applied'); // Apply new value to the field - await t.typeText(browserPage.treeViewDelimiterInput, 'test', { replace: true }); + await t.typeText(browserPage.TreeView.treeViewDelimiterInput, 'test', { replace: true }); // Click on Cancel button - await t.click(browserPage.treeViewDelimiterValueCancel); + await t.click(browserPage.TreeView.treeViewDelimiterValueCancel); // Check the previous delimiter value - await t.expect(browserPage.treeViewDelimiterButton.withExactText(':').exists).ok('Previous delimiter not applied'); + await t.click(browserPage.TreeView.treeViewSettingsBtn); + await t.expect(browserPage.TreeView.treeViewDelimiterInput.value).eql(':', 'Previous delimiter not applied'); + await t.click(browserPage.TreeView.treeViewDelimiterValueCancel); // Change delimiter - await browserPage.changeDelimiterInTreeView('-'); + await browserPage.TreeView.changeDelimiterInTreeView('-'); // Verify that when user changes the delimiter and clicks on Save button delimiter is applied - await browserPage.checkTreeViewFoldersStructure([['device_us', 'west'], ['mobile_eu', 'central'], ['mobile_us', 'east'], ['user_us', 'west'], ['device_eu', 'central'], ['user_eu', 'central']], '-', true); + await browserActions.checkTreeViewFoldersStructure([['device_us', 'west'], ['mobile_eu', 'central'], ['mobile_us', 'east'], ['user_us', 'west'], ['device_eu', 'central'], ['user_eu', 'central']], '-', true); }); diff --git a/tests/e2e/tests/web/critical-path/tree-view/tree-view-improvements.e2e.ts b/tests/e2e/tests/web/critical-path/tree-view/tree-view-improvements.e2e.ts index f3db07e435..ab6a43e7f7 100644 --- a/tests/e2e/tests/web/critical-path/tree-view/tree-view-improvements.e2e.ts +++ b/tests/e2e/tests/web/critical-path/tree-view/tree-view-improvements.e2e.ts @@ -5,13 +5,11 @@ import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { KeyTypesTexts, rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; -import { verifyKeysDisplayedInTheList, verifyKeysNotDisplayedInTheList } from '../../../../helpers/keys'; -import { APIKeyRequests } from '../../../../helpers/api/api-keys'; +import { verifyKeysDisplayingInTheList } from '../../../../helpers/keys'; const browserPage = new BrowserPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); -const apiKeyRequests = new APIKeyRequests(); let keyNames: string[]; let keyName1: string; @@ -27,12 +25,11 @@ test await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .after(async() => { - await t.click(browserPage.patternModeBtn); - await browserPage.deleteKeysByNames(keyNames); + await browserPage.Cli.sendCommandInCli('flushdb'); await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Tree view preselected folder', async t => { - keyName1 = Common.generateWord(10); // used to create index name - keyName2 = Common.generateWord(10); // used to create index name + keyName1 = Common.generateWord(10); + keyName2 = Common.generateWord(10); keyNameSingle = Common.generateWord(10); keyNames = [`${keyName1}:1`, `${keyName1}:2`, `${keyName2}:1`, `${keyName2}:2`, keyNameSingle]; @@ -41,7 +38,7 @@ test `HSET ${keyNames[0]} field value`, `HSET ${keyNames[1]} field value`, `HSET ${keyNames[2]} field value`, - `HSET ${keyNames[3]} field value`, + `SADD ${keyNames[3]} value`, `SADD ${keyNames[4]} value` ]; @@ -49,17 +46,17 @@ test await browserPage.Cli.sendCommandsInCli(commands); await t.click(browserPage.treeViewButton); // The folder without any patterns selected and the list of keys is displayed when there is a folder without any patterns - await verifyKeysDisplayedInTheList([keyNameSingle]); + await verifyKeysDisplayingInTheList([keyNameSingle], true); - await browserPage.openTreeFolders([await browserPage.getTextFromNthTreeElement(1)]); + await browserPage.TreeView.openTreeFolders([await browserPage.TreeView.getTextFromNthTreeElement(1)]); await browserPage.selectFilterGroupType(KeyTypesTexts.Set); // The folder without any namespaces is selected (if exists) when folder does not exist after search/filter - await verifyKeysDisplayedInTheList([keyNameSingle]); + await verifyKeysDisplayingInTheList([keyNameSingle], true); await browserPage.setAllKeyType(); // The folder without any patterns selected and the list of keys is displayed when there is a folder without any patterns - await verifyKeysDisplayedInTheList([keyNameSingle]); - await verifyKeysNotDisplayedInTheList([`${keyNames[0]}:1`, `${keyNames[2]}:2`]); + await verifyKeysDisplayingInTheList([keyNameSingle], true); + await verifyKeysDisplayingInTheList([`${keyNames[0]}:1`, `${keyNames[2]}:2`], false); // switch between browser view and tree view await t.click(browserPage.browserViewButton) @@ -67,57 +64,58 @@ test await browserPage.deleteKeyByName(keyNames[4]); await t.click(browserPage.clearFilterButton); // get first folder name - const firstTreeItemText = await browserPage.getTextFromNthTreeElement(0); - const firstTreeItemKeys = Selector(`[data-testid="node-item_${firstTreeItemText}:keys:keys:"]`); // keys after node item opened - // The first folder with namespaces is expanded and selected when there is no folder without any patterns - await t.expect(firstTreeItemKeys.visible) - .ok('First folder is not expanded'); - await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); + const firstTreeItemText = await browserPage.TreeView.getTextFromNthTreeElement(0); + // All folders with namespaces are collapsed when there is no folder without any patterns + await verifyKeysDisplayingInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`], false); const commands1 = [ - `HSET ${keyNames[4]} field value` + `SADD ${keyNames[4]} value` ]; await browserPage.Cli.sendCommandsInCli(commands1); await t.click(browserPage.refreshKeysButton); - // Refreshed Tree view preselected folder - await t.expect(firstTreeItemKeys.visible) - .ok('Folder is not selected'); - await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); + // Folders are collapsed after refresh + await verifyKeysDisplayingInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`], false); + await verifyKeysDisplayingInTheList([keyNameSingle], true); await browserPage.selectFilterGroupType(KeyTypesTexts.Hash); - await t.expect(firstTreeItemKeys.visible).ok('Folder is not selected after searching with HASH'); - // Filtered Tree view preselected folder - await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); + // Only folders according to key type filter are displayed + await verifyKeysDisplayingInTheList([keyNameSingle], false); + await browserPage.TreeView.verifyFolderDisplayingInTheList(firstTreeItemText, true); - await browserPage.searchByKeyName('*'); - // Search capability Filtered Tree view preselected folder - await t.expect(firstTreeItemKeys.visible).ok('Folder is not selected'); - await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); + await browserPage.searchByKeyName(`${keyName1}*`); + // Only folders according to filter by key names are displayed + await verifyKeysDisplayingInTheList([keyNameSingle], false); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, false); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); await t.click(browserPage.clearFilterButton); - // Filtered Tree view preselected folder - await t.expect(firstTreeItemKeys.visible).ok('Folder is not selected'); - await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); + // All folders are displayed and collapsed after cleared filter + await verifyKeysDisplayingInTheList([keyNameSingle], true); + await verifyKeysDisplayingInTheList([keyName1], false); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, true); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); await browserPage.selectFilterGroupType(KeyTypesTexts.Stream); - // Filtered Tree view preselected folder - await t.expect(browserPage.keyListTable.textContent).contains('No results found.', 'Key is not found message not displayed'); + // Verify that No results found message is displayed in case of invalid filtering + await t.expect(browserPage.noResultsFound.textContent).contains('No results found.', 'Key is not found message not displayed'); await browserPage.setAllKeyType(); // clear stream from filter - // Filtered Tree view preselected folder - await t.expect(browserPage.keyListTable.textContent).notContains('No results found.', 'Key is not found message still displayed'); - await t.expect( - firstTreeItemKeys.exists) - .notOk('First folder is expanded'); + // Verify that no results found message not displayed after clearing filter + await t.expect(browserPage.noResultsFound.exists).notOk('Key is not found message still displayed'); + // All folders are displayed and collapsed after cleared filter + await verifyKeysDisplayingInTheList([keyNameSingle], true); + await verifyKeysDisplayingInTheList([keyName1], false); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, true); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); }); - test .before(async() => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .after(async() => { await browserPage.Cli.sendCommandInCli(`FT.DROPINDEX ${index}`); + await browserPage.Cli.sendCommandInCli('flushdb'); await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify tree view navigation for index based search', async t => { keyName1 = Common.generateWord(10); // used to create index name @@ -139,24 +137,20 @@ test await t.click(browserPage.redisearchModeBtn); // click redisearch button await browserPage.selectIndexByName(index); await t.click(browserPage.treeViewButton); - await t.click(Selector(`[data-testid="${`node-item_${folders[0]}:`}"]`)); // close folder - await browserPage.openTreeFolders(folders); + await browserPage.TreeView.openTreeFolders(folders); await t.click(browserPage.refreshKeysButton); // Refreshed Tree view preselected folder for index based search await t.expect( Selector(`[data-testid="node-item_${folders[0]}:${folders[1]}:keys:keys:"]`).visible) .ok('Folder is not selected'); }); - test .before(async() => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .after(async() => { await t.click(browserPage.patternModeBtn); - for (const element of keyNames.slice(1)) { - await apiKeyRequests.deleteKeyByNameApi(element, ossStandaloneConfig.databaseName); - } + await browserPage.Cli.sendCommandInCli('flushdb'); await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Search capability Refreshed Tree view preselected folder', async t => { keyName1 = Common.generateWord(10); @@ -174,24 +168,30 @@ test await browserPage.Cli.sendCommandsInCli(commands); await t.click(browserPage.treeViewButton); // The folder without any patterns selected and the list of keys is displayed when there is a folder without any patterns - await verifyKeysDisplayedInTheList([keyNameSingle]); + await verifyKeysDisplayingInTheList([keyNameSingle], true); - await browserPage.openTreeFolders([keyName1]); // Type: hash - await browserPage.openTreeFolders([keyName2]); // Type: list + await browserPage.TreeView.openTreeFolders([keyName1]); // Type: hash + await browserPage.TreeView.openTreeFolders([keyName2]); // Type: list await browserPage.selectFilterGroupType(KeyTypesTexts.Hash); - // The first folder with namespaces is expanded and selected when folder and folder without any namespaces does not exist after search/filter - await verifyKeysDisplayedInTheList([keyNames[0], keyNames[1]]); + // Only related to key types filter folders are displayed + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, false); + await verifyKeysDisplayingInTheList([keyNames[0], keyNames[1]], false); await browserPage.setAllKeyType(); await browserPage.Cli.sendCommandsInCli([`DEL ${keyNames[0]}`]); await t.click(browserPage.refreshKeysButton); // refresh keys - // The previously selected folder is preselected when key does not exist after keys refresh - await verifyKeysDisplayedInTheList([keyNames[1]]); - await verifyKeysNotDisplayedInTheList([keyNames[0], keyNames[2], keyNames[3], keyNames[4]]); + // Only related to filter folders are displayed when key does not exist after keys refresh + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, true); + await verifyKeysDisplayingInTheList([keyNames[4]], true); + await verifyKeysDisplayingInTheList([keyNames[0], keyNames[2], keyNames[3]], false); await browserPage.searchByKeyName('*'); await t.click(browserPage.refreshKeysButton); // Search capability Refreshed Tree view preselected folder - await verifyKeysDisplayedInTheList([keyNames[1]]); - await verifyKeysNotDisplayedInTheList([keyNames[0], keyNames[2], keyNames[3], keyNames[4]]); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, true); + await verifyKeysDisplayingInTheList([keyNames[4]], true); + await verifyKeysDisplayingInTheList([keyNames[0], keyNames[2], keyNames[3]], false); }); diff --git a/tests/e2e/tests/web/critical-path/tree-view/tree-view.e2e.ts b/tests/e2e/tests/web/critical-path/tree-view/tree-view.e2e.ts index 74175a7508..aeab0c37b9 100644 --- a/tests/e2e/tests/web/critical-path/tree-view/tree-view.e2e.ts +++ b/tests/e2e/tests/web/critical-path/tree-view/tree-view.e2e.ts @@ -3,16 +3,11 @@ import { BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneBigConfig } from '../../../../helpers/conf'; import { rte, KeyTypesTexts } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; -import { Common } from '../../../../helpers/common'; import { verifySearchFilterValue } from '../../../../helpers/keys'; -import { APIKeyRequests } from '../../../../helpers/api/api-keys'; const browserPage = new BrowserPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); -const apiKeyRequests = new APIKeyRequests(); - -const keyNameFilter = `keyName${Common.generateWord(10)}`; fixture `Tree view verifications` .meta({ type: 'critical_path', rte: rte.standalone }) @@ -27,35 +22,17 @@ fixture `Tree view verifications` test('Verify that user can see that "Tree view" mode is enabled state is saved when refreshes the page', async t => { // Verify that when user opens the application he can see that Tree View is disabled by default(Browser is selected by default) await t.expect(browserPage.browserViewButton.getStyleProperty('background-color')).eql('rgb(41, 47, 71)', 'The Browser is not selected by default'); - await t.expect(browserPage.treeViewArea.exists).notOk('The tree view is displayed', { timeout: 10000 }); + await t.expect(browserPage.TreeView.treeViewSettingsBtn.exists).notOk('The tree view is displayed', { timeout: 5000 }); await t.click(browserPage.treeViewButton); await browserPage.reloadPage(); // Verify that "Tree view" mode enabled state is saved - await t.expect(browserPage.treeViewArea.exists).ok('The tree view is not displayed'); + await t.expect(browserPage.TreeView.treeViewSettingsBtn.exists).ok('The tree view is not displayed'); // Verify that user can scan DB by 10K in tree view await browserPage.verifyScannningMore(); }); -test - .after(async() => { - // Clear and delete database - await apiKeyRequests.deleteKeyByNameApi(keyNameFilter, ossStandaloneBigConfig.databaseName); - await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); - })('Verify that when user enables filtering by key name he can see only folder with appropriate keys are displayed and the number of keys and percentage is recalculated', async t => { - await browserPage.addHashKey(keyNameFilter); - await t.click(browserPage.treeViewButton); - const numberOfKeys = await browserPage.treeViewKeysNumber.textContent; - const percentage = await browserPage.treeViewPercentage.textContent; - // Set filter by key name - await browserPage.searchByKeyName(keyNameFilter); - await t.expect(browserPage.treeViewKeysItem.exists).ok('The key not appeared after the filtering', { timeout: 10000 }); - await t.click(browserPage.treeViewKeysItem); - // Verify the results - await t.expect(browserPage.treeViewKeysNumber.textContent).notEql(numberOfKeys, 'The number of keys is not recalculated'); - await t.expect(browserPage.treeViewPercentage.textContent).notEql(percentage, 'The percentage is not recalculated'); - await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyNameFilter)).ok('The appropriate keys are not displayed'); - }); +// outdated Verify that when user enables filtering by key name he can see only folder with appropriate keys are displayed and the number of keys and percentage is recalculated test('Verify that when user switched from Tree View to Browser and goes back state of filer by key name/key type is saved', async t => { const keyName = 'user*'; await t.click(browserPage.treeViewButton); diff --git a/tests/e2e/tests/web/regression/browser/filtering.e2e.ts b/tests/e2e/tests/web/regression/browser/filtering.e2e.ts index a637a6e8bc..abc2a24e04 100644 --- a/tests/e2e/tests/web/regression/browser/filtering.e2e.ts +++ b/tests/e2e/tests/web/regression/browser/filtering.e2e.ts @@ -142,10 +142,8 @@ test await browserPage.searchByKeyName(keyName); // Verify that required key is displayed await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).ok('Key not found'); - // Switch to tree view - await t.click(browserPage.treeViewButton); // Check searched key in tree view - await t.click(browserPage.treeViewNotPatternedKeys); + await t.click(browserPage.treeViewButton); await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).ok('Key not found'); }); test diff --git a/tests/e2e/tests/web/regression/tree-view/tree-view.e2e.ts b/tests/e2e/tests/web/regression/tree-view/tree-view.e2e.ts index 59628f00bc..8960514b38 100644 --- a/tests/e2e/tests/web/regression/tree-view/tree-view.e2e.ts +++ b/tests/e2e/tests/web/regression/tree-view/tree-view.e2e.ts @@ -53,24 +53,26 @@ test('Verify that user can see the total number of keys, the number of keys scan await t.expect(browserPage.scanMoreButton.visible).ok('The scan more button is not displayed on the Tree view'); }); test('Verify that when user deletes the key he can see the key is removed from the folder, the number of keys is reduced, the percentage is recalculated', async t => { + const mainFolder = browserPage.TreeView.getFolderSelectorByName('device'); // Open the first key in the tree view and remove await t.click(browserPage.treeViewButton); - // Verify the default separator - await t.expect(browserPage.treeViewSeparator.textContent).eql(':', 'The “:” (colon) not used as a default separator for namespaces'); + await t.click(browserPage.TreeView.treeViewSettingsBtn); + await t.expect(browserPage.TreeView.treeViewDelimiterInput.value).eql(':', 'The “:” (colon) not used as a default separator for namespaces'); // Verify that user can see that “:” (colon) used as a default separator for namespaces and see the number of keys found per each namespace - await t.expect(browserPage.treeViewKeysNumber.visible).ok('The user can not see the number of keys'); + await t.expect(browserPage.TreeView.treeViewKeysNumber.visible).ok('The user can not see the number of keys'); - await t.expect(browserPage.treeViewDeviceFolder.visible).ok('The key folder is not displayed', { timeout: 30000 }); - await t.click(browserPage.treeViewDeviceFolder); - const numberOfKeys = await browserPage.treeViewDeviceKyesCount.textContent; - const keyFolder = await browserPage.treeViewDeviceFolder.nth(2).textContent; - await t.click(browserPage.treeViewDeviceFolder.nth(2)); - await t.click(browserPage.treeViewDeviceFolder.nth(5)); + await t.expect(mainFolder.visible).ok('The key folder is not displayed'); + await t.click(mainFolder); + const numberOfKeys = await browserPage.TreeView.getFolderCountSelectorByName('device').textContent; + const targetFolderName = await mainFolder.nth(1).find(`[data-testid^=folder-]`).textContent; + const targetFolderSelector = browserPage.TreeView.getFolderSelectorByName(`device:${targetFolderName}`); + await t.click(targetFolderSelector); await browserPage.deleteKey(); // Verify the results - await t.expect(browserPage.treeViewDeviceFolder.nth(2).exists).notOk('The previous folder is not closed after removing key folder'); - await t.click(browserPage.treeViewDeviceFolder); - await t.expect(browserPage.treeViewDeviceFolder.nth(2).textContent).notEql(keyFolder, 'The key folder is not removed from the tree view'); - await t.expect(browserPage.treeViewDeviceKyesCount.textContent).notEql(numberOfKeys, 'The number of keys is not recalculated'); + await t.expect(targetFolderSelector.exists).notOk('The previous folder is not closed after removing key folder'); + await t.click(browserPage.TreeView.treeViewDeviceFolder); + await t.expect(mainFolder.nth(1).textContent).notEql(targetFolderName, 'The key folder is not removed from the tree view'); + const actualCount = await browserPage.TreeView.getFolderCountSelectorByName('device').textContent; + await t.expect(+actualCount).lt(+numberOfKeys, 'The number of keys is not recalculated'); }); diff --git a/tests/e2e/tests/web/regression/workbench/import-tutorials.e2e.ts b/tests/e2e/tests/web/regression/workbench/import-tutorials.e2e.ts index d46d3c39d7..af56e3c824 100644 --- a/tests/e2e/tests/web/regression/workbench/import-tutorials.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/import-tutorials.e2e.ts @@ -6,7 +6,7 @@ import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pag import { commonUrl, ossStandaloneConfig, ossStandaloneRedisearch } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; -import { deleteAllKeysFromDB, verifyKeysDisplayedInTheList } from '../../../../helpers/keys'; +import { deleteAllKeysFromDB, verifyKeysDisplayingInTheList } from '../../../../helpers/keys'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); @@ -205,5 +205,5 @@ test await t.click(myRedisDatabasePage.NavigationPanel.browserButton); // Verify that keys of all types can be uploaded await browserPage.searchByKeyName('*key1*'); - await verifyKeysDisplayedInTheList(keyNames); + await verifyKeysDisplayingInTheList(keyNames, true); }); From bd208754c1a211bebcc2d1cb9ae100f02c3de7d0 Mon Sep 17 00:00:00 2001 From: zalenskiSofteq Date: Tue, 31 Oct 2023 14:50:04 +0100 Subject: [PATCH 06/12] * #RI-5095 - [FE] Scrollbar is displayed for "No keys" panel in tree view when clicked * #RI-5096 - [FE] Delete popup moved at the top left corner when hovering on the key below in list and delete popup opened * #RI-5097 - [FE] Multiple tooltips can be displayed at the same time when hovering over different namespaces after scrolling * #RI-5098 - [FE] Key details are opened for the key in tree view automatically when it wasn't opened in list view * #RI-5100 - [FE] Namespaces folders collapsed after refresh --- .../components/keys-header/KeysHeader.tsx | 1 - .../no-keys-found/styles.module.scss | 1 + .../components/virtual-tree/VirtualTree.tsx | 1 + .../virtual-tree/components/Node/Node.tsx | 70 ++++++++----------- .../components/Node/styles.module.scss | 7 +- .../components/virtual-tree/interfaces.ts | 1 + yarn.lock | 21 +----- 7 files changed, 40 insertions(+), 62 deletions(-) diff --git a/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx b/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx index 04a3f9795f..24102f588f 100644 --- a/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx +++ b/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx @@ -117,7 +117,6 @@ const KeysHeader = (props: Props) => { } const handleRefreshKeys = () => { - dispatch(resetBrowserTree()) dispatch(fetchKeys( { searchMode, diff --git a/redisinsight/ui/src/pages/browser/components/no-keys-found/styles.module.scss b/redisinsight/ui/src/pages/browser/components/no-keys-found/styles.module.scss index 73c3a47969..d747185b18 100644 --- a/redisinsight/ui/src/pages/browser/components/no-keys-found/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/no-keys-found/styles.module.scss @@ -1,5 +1,6 @@ .container { max-width: 400px; + min-height: 440px; margin: auto; text-align: center; diff --git a/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.tsx b/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.tsx index 54cd1795b5..552873d9b2 100644 --- a/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.tsx +++ b/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.tsx @@ -187,6 +187,7 @@ const VirtualTree = (props: Props) => { size: node.size, type: node.type, fullName: node.fullName, + shortName: node.nameString?.split(delimiter).pop(), nestingLevel, deleting, path: node.path, diff --git a/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.tsx b/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.tsx index a0ae1b7278..8739c9c7a8 100644 --- a/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.tsx +++ b/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.tsx @@ -41,6 +41,7 @@ const Node = ({ path, type, ttl, + shortName, size, deleting, nameString, @@ -64,12 +65,6 @@ const Node = ({ } }, []) - useEffect(() => { - if (isSelected && nameBuffer) { - updateStatusSelected?.(nameBuffer) - } - }, [isSelected]) - const handleClick = () => { if (isLeaf && !isSelected) { updateStatusSelected?.(nameBuffer) @@ -98,35 +93,41 @@ const Node = ({ } const Folder = () => ( - <> -
- - - - {nameString} - -
-
-
- {keyApproximate ? `${keyApproximate < 1 ? '<1' : Math.round(keyApproximate)}%` : '' } + + <> +
+ + + + {nameString} +
-
{keyCount ?? ''}
-
- +
+
+ {keyApproximate ? `${keyApproximate < 1 ? '<1' : Math.round(keyApproximate)}%` : '' } +
+
{keyCount ?? ''}
+
+ + ) const Leaf = () => ( <> - + - {isLeaf && Node} - {!isLeaf && ( - - {Node} - - )} + {Node}
) } diff --git a/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/styles.module.scss b/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/styles.module.scss index 653a14ad0b..dd3be8344d 100644 --- a/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/styles.module.scss @@ -1,8 +1,9 @@ .anchorTooltipNode { width: 100%; height: 42px; - display: inline-block; + display: flex !important; position: relative; + align-items: center; } .nodeContainer { @@ -39,13 +40,13 @@ :global(.moveOnHoverKey) { transition: transform ease 0.3s; - &.hide { + &:global(.hide) { transform: translateX(-8px); } } :global(.showOnHoverKey) { display: none !important; - &.show { + &:global(.show) { display: flex !important; } } diff --git a/redisinsight/ui/src/pages/browser/components/virtual-tree/interfaces.ts b/redisinsight/ui/src/pages/browser/components/virtual-tree/interfaces.ts index 8d34865532..119c843d29 100644 --- a/redisinsight/ui/src/pages/browser/components/virtual-tree/interfaces.ts +++ b/redisinsight/ui/src/pages/browser/components/virtual-tree/interfaces.ts @@ -42,6 +42,7 @@ export interface TreeData extends FixedSizeNodeData { keyCount: number keyApproximate: number fullName: string + shortName?: string leafIcon: string type: KeyTypes | ModulesKeyTypes ttl: number diff --git a/yarn.lock b/yarn.lock index 5cf651da51..5f279b5887 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11797,7 +11797,7 @@ postcss-reduce-transforms@^5.1.0: dependencies: postcss-value-parser "^4.2.0" -postcss-selector-parser@^6.0.2: +postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9: version "6.0.13" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz#d05d8d76b1e8e173257ef9d60b706a8e5e99bf1b" integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ== @@ -11805,14 +11805,6 @@ postcss-selector-parser@^6.0.2: cssesc "^3.0.0" util-deprecate "^1.0.2" -postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9: - version "6.0.11" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc" - integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - postcss-svgo@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.1.0.tgz#0a317400ced789f233a28826e77523f15857d80d" @@ -11833,7 +11825,7 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.2.15: +postcss@^8.2.15, postcss@^8.2.9: version "8.4.31" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== @@ -11842,15 +11834,6 @@ postcss@^8.2.15: picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@^8.2.9: - version "8.4.23" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.23.tgz#df0aee9ac7c5e53e1075c24a3613496f9e6552ab" - integrity sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA== - dependencies: - nanoid "^3.3.6" - picocolors "^1.0.0" - source-map-js "^1.0.2" - postinstall-postinstall@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz#4f7f77441ef539d1512c40bd04c71b06a4704ca3" From b0a457a898c666c4147686de0f1416398e61fb92 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Tue, 31 Oct 2023 16:57:35 +0100 Subject: [PATCH 07/12] add tests on sorting and fixes --- tests/e2e/pageObjects/browser-page.ts | 2 +- .../components/browser/tree-view.ts | 48 +++++++++++-- .../tree-view/tree-view-improvements.e2e.ts | 6 +- .../tree-view/tree-view-improvements.e2e.ts | 4 +- .../web/regression/browser/add-keys.e2e.ts | 4 +- .../web/regression/tree-view/tree-view.e2e.ts | 71 ++++++++++++++++++- 6 files changed, 121 insertions(+), 14 deletions(-) diff --git a/tests/e2e/pageObjects/browser-page.ts b/tests/e2e/pageObjects/browser-page.ts index 558bdd890a..5de04d558f 100644 --- a/tests/e2e/pageObjects/browser-page.ts +++ b/tests/e2e/pageObjects/browser-page.ts @@ -207,7 +207,7 @@ export class BrowserPage extends InstancePage { jsonKeyValue = Selector('[data-testid=json-data]'); jsonError = Selector('[data-testid=edit-json-error]'); tooltip = Selector('[role=tooltip]'); - noResultsFound = Selector('[data-testid=no-result-found-only]'); + noResultsFound = Selector('[data-test-subj=no-result-found]'); searchAdvices = Selector('[data-test-subj=search-advices]'); keysNumberOfResults = Selector('[data-testid=keys-number-of-results]'); keysTotalNumber = Selector('[data-testid=keys-total]'); diff --git a/tests/e2e/pageObjects/components/browser/tree-view.ts b/tests/e2e/pageObjects/components/browser/tree-view.ts index 0472b24f6b..d3fd5c75ae 100644 --- a/tests/e2e/pageObjects/components/browser/tree-view.ts +++ b/tests/e2e/pageObjects/components/browser/tree-view.ts @@ -1,4 +1,5 @@ import { Selector, t } from 'testcafe'; +import { Common } from '../../../helpers/common'; export class TreeView { //------------------------------------------------------------------------------------------- @@ -11,13 +12,16 @@ export class TreeView { treeViewSettingsBtn = Selector('[data-testid=tree-view-settings-btn]'); treeViewDelimiterValueSave = Selector('[data-testid=tree-view-apply-btn]'); treeViewDelimiterValueCancel = Selector('[data-testid=tree-view-cancel-btn]'); + sortingBtn = Selector('[data-testid=tree-view-sorting-select]'); + sortingASCoption = Selector('[id=ASC]'); + sortingDESCoption = Selector('[id=DESC]'); + sortingProgressBar = Selector('[data-testid=progress-key-tree]'); // TEXT ELEMENTS treeViewKeysNumber = Selector('[data-testid^=count_]'); treeViewDeviceFolder = Selector('[data-testid^=node-item_device] div'); //INPUTS treeViewDelimiterInput = Selector('[data-testid=tree-view-delimiter-input]'); - /** * Get folder selector by folder name * @param folderName The name of the folder @@ -31,8 +35,8 @@ export class TreeView { * @param folderName The name of the folder */ getFolderCountSelectorByName(folderName: string): Selector { - return Selector(`[data-testid^="count_${folderName}"]`); - } + return Selector(`[data-testid^="count_${folderName}"]`); + } /** * Verifying if the Keys are in the List of keys @@ -40,7 +44,7 @@ export class TreeView { * @param isDisplayed True if keys should be displayed */ async verifyFolderDisplayingInTheList(folderName: string, isDisplayed: boolean): Promise { - isDisplayed + isDisplayed ? await t.expect(this.getFolderSelectorByName(folderName).exists).ok(`The folder ${folderName} not found`) : await t.expect(this.getFolderSelectorByName(folderName).exists).notOk(`The folder ${folderName} found`); } @@ -58,6 +62,23 @@ export class TreeView { await t.click(this.treeViewDelimiterValueSave); } + /** + * Change ordering value + * @param order ASC/DESC ordering for tree view + */ + async changeOrderingInTreeView(order: string): Promise { + // Open settings popup + await t.click(this.treeViewSettingsBtn); + await t.click(this.sortingBtn); + order === 'ASC' + ? await t.click(this.sortingASCoption) + : await t.click(this.sortingDESCoption) + + // Click on save button + await t.click(this.treeViewDelimiterValueSave); + await Common.waitForElementNotVisible(this.sortingProgressBar); + } + /** * Get text from tree element by number * @param number The number of tree folder @@ -80,4 +101,23 @@ export class TreeView { } } } + + /** + * Get all keys from tree view list with order + */ + async getAllItemsArray(): Promise { + const textArray: string[] = []; + const treeViewItemElements = Selector('[role="treeitem"]'); + const itemCount = await treeViewItemElements.count; + + for (let i = 0; i < itemCount; i++) { + const treeItem = treeViewItemElements.nth(i); + const keyItem = treeItem.find('[data-testid^="key-"]'); + if (await keyItem.exists) { + textArray.push(await keyItem.textContent); + } + } + + return textArray; + } } diff --git a/tests/e2e/tests/electron/critical-path/tree-view/tree-view-improvements.e2e.ts b/tests/e2e/tests/electron/critical-path/tree-view/tree-view-improvements.e2e.ts index 4a5126f300..7b2a83ab00 100644 --- a/tests/e2e/tests/electron/critical-path/tree-view/tree-view-improvements.e2e.ts +++ b/tests/e2e/tests/electron/critical-path/tree-view/tree-view-improvements.e2e.ts @@ -117,6 +117,7 @@ test }) .after(async() => { await browserPage.Cli.sendCommandInCli(`FT.DROPINDEX ${index}`); + await browserPage.Cli.sendCommandInCli('flushdb'); await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify tree view navigation for index based search', async t => { keyName1 = Common.generateWord(10); // used to create index name @@ -138,13 +139,10 @@ test await t.click(browserPage.redisearchModeBtn); // click redisearch button await browserPage.selectIndexByName(index); await t.click(browserPage.treeViewButton); - await t.click(Selector(`[data-testid="${`node-item_${folders[0]}:`}"]`)); // close folder await browserPage.TreeView.openTreeFolders(folders); await t.click(browserPage.refreshKeysButton); // Refreshed Tree view preselected folder for index based search - await t.expect( - Selector(`[data-testid="node-item_${folders[0]}:${folders[1]}:keys:keys:"]`).visible) - .ok('Folder is not selected'); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); }); test diff --git a/tests/e2e/tests/web/critical-path/tree-view/tree-view-improvements.e2e.ts b/tests/e2e/tests/web/critical-path/tree-view/tree-view-improvements.e2e.ts index ab6a43e7f7..574533dfb6 100644 --- a/tests/e2e/tests/web/critical-path/tree-view/tree-view-improvements.e2e.ts +++ b/tests/e2e/tests/web/critical-path/tree-view/tree-view-improvements.e2e.ts @@ -140,9 +140,7 @@ test await browserPage.TreeView.openTreeFolders(folders); await t.click(browserPage.refreshKeysButton); // Refreshed Tree view preselected folder for index based search - await t.expect( - Selector(`[data-testid="node-item_${folders[0]}:${folders[1]}:keys:keys:"]`).visible) - .ok('Folder is not selected'); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); }); test .before(async() => { diff --git a/tests/e2e/tests/web/regression/browser/add-keys.e2e.ts b/tests/e2e/tests/web/regression/browser/add-keys.e2e.ts index 82a11fb17f..5f9dbcd53e 100644 --- a/tests/e2e/tests/web/regression/browser/add-keys.e2e.ts +++ b/tests/e2e/tests/web/regression/browser/add-keys.e2e.ts @@ -5,6 +5,7 @@ import { commonUrl, ossStandaloneBigConfig, ossStandaloneConfig } from '../../.. import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; import { BrowserActions } from '../../../../common-actions/browser-actions'; +import { verifyKeysDisplayingInTheList } from '../../../../helpers/keys'; const browserPage = new BrowserPage(); const browserActions = new BrowserActions(); @@ -96,7 +97,8 @@ test await t.click(browserPage.treeViewButton); await browserPage.addHashKey(keyName3); // Verify that user can see Tree view recalculated when new key is added in Tree view - await browserActions.verifyKeyDisplayedTopAndOpened(keyName3); + await browserActions.verifyKeyIsNotDisplayedTop(keyName3); + await verifyKeysDisplayingInTheList([keyName3], true); await t.click(browserPage.redisearchModeBtn); await browserPage.selectIndexByName(indexName); diff --git a/tests/e2e/tests/web/regression/tree-view/tree-view.e2e.ts b/tests/e2e/tests/web/regression/tree-view/tree-view.e2e.ts index 8960514b38..75db76c555 100644 --- a/tests/e2e/tests/web/regression/tree-view/tree-view.e2e.ts +++ b/tests/e2e/tests/web/regression/tree-view/tree-view.e2e.ts @@ -3,15 +3,21 @@ import { BrowserPage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneBigConfig, + ossStandaloneConfigEmpty, ossStandaloneRedisearch } from '../../../../helpers/conf'; import { KeyTypesTexts, rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; +import { APIKeyRequests } from '../../../../helpers/api/api-keys'; +import { Common } from '../../../../helpers/common'; const browserPage = new BrowserPage(); const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const apiKeyRequests = new APIKeyRequests(); + +let keyNames: string[] = []; fixture `Tree view verifications` .meta({ type: 'regression', rte: rte.standalone }) @@ -65,7 +71,7 @@ test('Verify that when user deletes the key he can see the key is removed from t await t.expect(mainFolder.visible).ok('The key folder is not displayed'); await t.click(mainFolder); const numberOfKeys = await browserPage.TreeView.getFolderCountSelectorByName('device').textContent; - const targetFolderName = await mainFolder.nth(1).find(`[data-testid^=folder-]`).textContent; + const targetFolderName = await mainFolder.nth(1).find('[data-testid^=folder-]').textContent; const targetFolderSelector = browserPage.TreeView.getFolderSelectorByName(`device:${targetFolderName}`); await t.click(targetFolderSelector); await browserPage.deleteKey(); @@ -76,3 +82,66 @@ test('Verify that when user deletes the key he can see the key is removed from t const actualCount = await browserPage.TreeView.getFolderCountSelectorByName('device').textContent; await t.expect(+actualCount).lt(+numberOfKeys, 'The number of keys is not recalculated'); }); +test + .before(async() => { + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfigEmpty); + }) + .after(async() => { + // Clear and delete database + for(const name of keyNames) { + await apiKeyRequests.deleteKeyByNameApi(name, ossStandaloneConfigEmpty.databaseName); + } + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfigEmpty); + })('Verify that if there are keys without namespaces, they are displayed in the root directory after all folders by default in the Tree view', async t => { + keyNames = [ + `atest:a-${Common.generateWord(10)}`, + `atest:z-${Common.generateWord(10)}`, + `ztest:a-${Common.generateWord(10)}`, + `ztest:z-${Common.generateWord(10)}`, + `atest-${Common.generateWord(10)}`, + `ztest-${Common.generateWord(10)}` + ]; + const commands = [ + 'flushdb', + `HSET ${keyNames[0]} field value`, + `HSET ${keyNames[1]} field value`, + `HSET ${keyNames[2]} field value`, + `SADD ${keyNames[3]} value`, + `SADD ${keyNames[4]} value`, + `HSET ${keyNames[5]} field value` + ]; + const expectedSortedByASC = [ + keyNames[0].split(':')[1], + keyNames[1].split(':')[1], + keyNames[2].split(':')[1], + keyNames[3].split(':')[1], + keyNames[4], + keyNames[5] + ]; + const expectedSortedByDESC = [ + keyNames[3].split(':')[1], + keyNames[2].split(':')[1], + keyNames[1].split(':')[1], + keyNames[0].split(':')[1], + keyNames[5], + keyNames[4] + ]; + + // Create 5 keys + await browserPage.Cli.sendCommandsInCli(commands); + await t.click(browserPage.treeViewButton); + + // Verify that if there are keys without namespaces, they are displayed in the root directory after all folders by default in the Tree view + await browserPage.TreeView.openTreeFolders([`${keyNames[0]}`.split(':')[0]]); + await browserPage.TreeView.openTreeFolders([`${keyNames[2]}`.split(':')[0]]); + let actualItemsArray = await browserPage.TreeView.getAllItemsArray(); + // Verify that user can see all folders and keys sorted by name ASC by default + await t.expect(actualItemsArray).eql(expectedSortedByASC); + + // Verify that user can change the sorting ASC-DESC + await browserPage.TreeView.changeOrderingInTreeView('DESC'); + await browserPage.TreeView.openTreeFolders([`${keyNames[2]}`.split(':')[0]]); + await browserPage.TreeView.openTreeFolders([`${keyNames[0]}`.split(':')[0]]); + actualItemsArray = await browserPage.TreeView.getAllItemsArray(); + await t.expect(actualItemsArray).eql(expectedSortedByDESC); + }); From cacbec78ed519838013252e834306da9a6b8a096 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Tue, 31 Oct 2023 18:07:59 +0100 Subject: [PATCH 08/12] fixes after bugfix --- tests/e2e/common-actions/browser-actions.ts | 2 +- tests/e2e/desktop.runner.ts | 1 + tests/e2e/desktop.runner.win.ts | 1 + tests/e2e/pageObjects/browser-page.ts | 1 + .../critical-path/tree-view/tree-view-improvements.e2e.ts | 4 ++-- .../web/critical-path/tree-view/tree-view-improvements.e2e.ts | 4 ++-- tests/e2e/tests/web/regression/browser/add-keys.e2e.ts | 2 +- 7 files changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/e2e/common-actions/browser-actions.ts b/tests/e2e/common-actions/browser-actions.ts index a0afa741ae..847537a906 100644 --- a/tests/e2e/common-actions/browser-actions.ts +++ b/tests/e2e/common-actions/browser-actions.ts @@ -94,7 +94,7 @@ export class BrowserActions { // Verify that the last folder level contains required keys const foundKeyName = `${folders[i].join(delimiter)}`; await t - .expect(Selector(`[data-testid*="key-${foundKeyName}"]`).exists).ok('Specific key not found') + .expect(Selector(`[data-testid*="node-item_${foundKeyName}"]`).find('[data-testid^="key-"]').exists).ok('Specific key not found') .click(array[0]); } } diff --git a/tests/e2e/desktop.runner.ts b/tests/e2e/desktop.runner.ts index 11444f3694..a0f6be3b94 100644 --- a/tests/e2e/desktop.runner.ts +++ b/tests/e2e/desktop.runner.ts @@ -38,6 +38,7 @@ import testcafe from 'testcafe'; selectorTimeout: 5000, assertionTimeout: 5000, speed: 1, + quarantineMode: { successThreshold: 1, attemptLimit: 3 }, }); }) .then((failedCount) => { diff --git a/tests/e2e/desktop.runner.win.ts b/tests/e2e/desktop.runner.win.ts index 20ec80beb1..5755eeb38d 100644 --- a/tests/e2e/desktop.runner.win.ts +++ b/tests/e2e/desktop.runner.win.ts @@ -38,6 +38,7 @@ import testcafe from 'testcafe'; selectorTimeout: 5000, assertionTimeout: 5000, speed: 1, + quarantineMode: { successThreshold: 1, attemptLimit: 3 }, }); }) .then((failedCount) => { diff --git a/tests/e2e/pageObjects/browser-page.ts b/tests/e2e/pageObjects/browser-page.ts index 5de04d558f..98787eb7bf 100644 --- a/tests/e2e/pageObjects/browser-page.ts +++ b/tests/e2e/pageObjects/browser-page.ts @@ -208,6 +208,7 @@ export class BrowserPage extends InstancePage { jsonError = Selector('[data-testid=edit-json-error]'); tooltip = Selector('[role=tooltip]'); noResultsFound = Selector('[data-test-subj=no-result-found]'); + noResultsFoundOnly = Selector('[data-testid=no-result-found-only]'); searchAdvices = Selector('[data-test-subj=search-advices]'); keysNumberOfResults = Selector('[data-testid=keys-number-of-results]'); keysTotalNumber = Selector('[data-testid=keys-total]'); diff --git a/tests/e2e/tests/electron/critical-path/tree-view/tree-view-improvements.e2e.ts b/tests/e2e/tests/electron/critical-path/tree-view/tree-view-improvements.e2e.ts index 7b2a83ab00..fd83380795 100644 --- a/tests/e2e/tests/electron/critical-path/tree-view/tree-view-improvements.e2e.ts +++ b/tests/e2e/tests/electron/critical-path/tree-view/tree-view-improvements.e2e.ts @@ -100,11 +100,11 @@ test await browserPage.selectFilterGroupType(KeyTypesTexts.Stream); // Verify that No results found message is displayed in case of invalid filtering - await t.expect(browserPage.noResultsFound.textContent).contains('No results found.', 'Key is not found message not displayed'); + await t.expect(browserPage.noResultsFoundOnly.textContent).contains('No results found.', 'Key is not found message not displayed'); await browserPage.setAllKeyType(); // clear stream from filter // Verify that no results found message not displayed after clearing filter - await t.expect(browserPage.noResultsFound.exists).notOk('Key is not found message still displayed'); + await t.expect(browserPage.noResultsFoundOnly.exists).notOk('Key is not found message still displayed'); // All folders are displayed and collapsed after cleared filter await verifyKeysDisplayingInTheList([keyNameSingle], true); await verifyKeysDisplayingInTheList([keyName1], false); diff --git a/tests/e2e/tests/web/critical-path/tree-view/tree-view-improvements.e2e.ts b/tests/e2e/tests/web/critical-path/tree-view/tree-view-improvements.e2e.ts index 574533dfb6..5fbceb27a4 100644 --- a/tests/e2e/tests/web/critical-path/tree-view/tree-view-improvements.e2e.ts +++ b/tests/e2e/tests/web/critical-path/tree-view/tree-view-improvements.e2e.ts @@ -98,11 +98,11 @@ test await browserPage.selectFilterGroupType(KeyTypesTexts.Stream); // Verify that No results found message is displayed in case of invalid filtering - await t.expect(browserPage.noResultsFound.textContent).contains('No results found.', 'Key is not found message not displayed'); + await t.expect(browserPage.noResultsFoundOnly.textContent).contains('No results found.', 'Key is not found message not displayed'); await browserPage.setAllKeyType(); // clear stream from filter // Verify that no results found message not displayed after clearing filter - await t.expect(browserPage.noResultsFound.exists).notOk('Key is not found message still displayed'); + await t.expect(browserPage.noResultsFoundOnly.exists).notOk('Key is not found message still displayed'); // All folders are displayed and collapsed after cleared filter await verifyKeysDisplayingInTheList([keyNameSingle], true); await verifyKeysDisplayingInTheList([keyName1], false); diff --git a/tests/e2e/tests/web/regression/browser/add-keys.e2e.ts b/tests/e2e/tests/web/regression/browser/add-keys.e2e.ts index 5f9dbcd53e..4da14a05ab 100644 --- a/tests/e2e/tests/web/regression/browser/add-keys.e2e.ts +++ b/tests/e2e/tests/web/regression/browser/add-keys.e2e.ts @@ -98,7 +98,7 @@ test await browserPage.addHashKey(keyName3); // Verify that user can see Tree view recalculated when new key is added in Tree view await browserActions.verifyKeyIsNotDisplayedTop(keyName3); - await verifyKeysDisplayingInTheList([keyName3], true); + await t.expect(browserPage.keyNameFormDetails.withExactText(keyName3).exists).ok(`Key ${keyName3} details not opened`); await t.click(browserPage.redisearchModeBtn); await browserPage.selectIndexByName(indexName); From 6d4b10072b10cce3c48d6dab4ea94ab9c3a502c7 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Tue, 31 Oct 2023 18:59:34 +0100 Subject: [PATCH 09/12] fix --- .../tree-view/tree-view-improvements.e2e.ts | 28 ++++++++++--------- .../tree-view/tree-view-improvements.e2e.ts | 2 +- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/tests/e2e/tests/electron/critical-path/tree-view/tree-view-improvements.e2e.ts b/tests/e2e/tests/electron/critical-path/tree-view/tree-view-improvements.e2e.ts index fd83380795..1daae0eb76 100644 --- a/tests/e2e/tests/electron/critical-path/tree-view/tree-view-improvements.e2e.ts +++ b/tests/e2e/tests/electron/critical-path/tree-view/tree-view-improvements.e2e.ts @@ -1,4 +1,4 @@ -import { Selector, t } from 'testcafe'; +import { t } from 'testcafe'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; @@ -6,12 +6,10 @@ import { KeyTypesTexts, rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; import { verifyKeysDisplayingInTheList } from '../../../../helpers/keys'; -import { APIKeyRequests } from '../../../../helpers/api/api-keys'; const browserPage = new BrowserPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); -const apiKeyRequests = new APIKeyRequests(); let keyNames: string[]; let keyName1: string; @@ -151,9 +149,7 @@ test }) .after(async() => { await t.click(browserPage.patternModeBtn); - for (const element of keyNames.slice(1)) { - await apiKeyRequests.deleteKeyByNameApi(element, ossStandaloneConfig.databaseName); - } + await browserPage.Cli.sendCommandInCli('flushdb'); await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Search capability Refreshed Tree view preselected folder', async t => { keyName1 = Common.generateWord(10); @@ -176,19 +172,25 @@ test await browserPage.TreeView.openTreeFolders([keyName1]); // Type: hash await browserPage.TreeView.openTreeFolders([keyName2]); // Type: list await browserPage.selectFilterGroupType(KeyTypesTexts.Hash); - // The first folder with namespaces is expanded and selected when folder and folder without any namespaces does not exist after search/filter - await verifyKeysDisplayingInTheList([keyNames[0], keyNames[1]], true); + // Only related to key types filter folders are displayed + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, false); + await verifyKeysDisplayingInTheList([keyNames[0], keyNames[1]], false); await browserPage.setAllKeyType(); await browserPage.Cli.sendCommandsInCli([`DEL ${keyNames[0]}`]); await t.click(browserPage.refreshKeysButton); // refresh keys - // The previously selected folder is preselected when key does not exist after keys refresh - await verifyKeysDisplayingInTheList([keyNames[1]], true); - await verifyKeysDisplayingInTheList([keyNames[0], keyNames[2], keyNames[3], keyNames[4]], false); + // Only related to filter folders are displayed when key does not exist after keys refresh + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, true); + await verifyKeysDisplayingInTheList([keyNames[4]], true); + await verifyKeysDisplayingInTheList([keyNames[0], keyNames[2], keyNames[3]], false); await browserPage.searchByKeyName('*'); await t.click(browserPage.refreshKeysButton); // Search capability Refreshed Tree view preselected folder - await verifyKeysDisplayingInTheList([keyNames[1]], true); - await verifyKeysDisplayingInTheList([keyNames[0], keyNames[2], keyNames[3], keyNames[4]], false); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, true); + await verifyKeysDisplayingInTheList([keyNames[4]], true); + await verifyKeysDisplayingInTheList([keyNames[0], keyNames[2], keyNames[3]], false); }); diff --git a/tests/e2e/tests/web/critical-path/tree-view/tree-view-improvements.e2e.ts b/tests/e2e/tests/web/critical-path/tree-view/tree-view-improvements.e2e.ts index 5fbceb27a4..30bad9bec1 100644 --- a/tests/e2e/tests/web/critical-path/tree-view/tree-view-improvements.e2e.ts +++ b/tests/e2e/tests/web/critical-path/tree-view/tree-view-improvements.e2e.ts @@ -1,4 +1,4 @@ -import { Selector, t } from 'testcafe'; +import { t } from 'testcafe'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; From 84a379d0079e804c576012f64f479206af5459b6 Mon Sep 17 00:00:00 2001 From: Zalenski Egor Date: Wed, 1 Nov 2023 01:35:15 +0300 Subject: [PATCH 10/12] Update Node.spec.tsx --- .../components/virtual-tree/components/Node/Node.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.spec.tsx b/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.spec.tsx index 588cd1df4b..a5df9953fe 100644 --- a/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.spec.tsx @@ -107,7 +107,7 @@ describe('Node', () => { expect(mockSetOpen).not.toBeCalled() }) - it.only('name, ttl and size should be rendered', () => { + it('name, ttl and size should be rendered', () => { const { getByTestId } = render( Date: Wed, 1 Nov 2023 10:32:43 +0100 Subject: [PATCH 11/12] fixes by pr comments --- tests/e2e/common-actions/browser-actions.ts | 64 ++++++++++++------- .../critical-path/tree-view/delimiter.e2e.ts | 2 +- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/tests/e2e/common-actions/browser-actions.ts b/tests/e2e/common-actions/browser-actions.ts index 847537a906..a9275cffba 100644 --- a/tests/e2e/common-actions/browser-actions.ts +++ b/tests/e2e/common-actions/browser-actions.ts @@ -59,38 +59,54 @@ export class BrowserActions { } /** - * Check tree view structure - * @folders name of folders for tree view build - * @delimiter string with delimiter value - * @commonKeyFolder flag if not patterned keys will be displayed + * Verify that not patterned keys not visible with delimiter + * @param delimiter string with delimiter value */ - async checkTreeViewFoldersStructure(folders: string[][], delimiter: string, commonKeyFolder: boolean): Promise { - // Verify that all keys that are not inside of tree view doesn't contain delimiter - if (commonKeyFolder) { - const notPatternedKeys = Selector('[data-testid^="badge"]').parent('[data-testid^="node-item_"]'); - const notPatternedKeysNumber = await notPatternedKeys.count; - for (let i = 0; i < notPatternedKeysNumber; i++) { - await t.expect(notPatternedKeys.nth(i).withText(delimiter).exists).notOk('Not contained delimiter keys'); - } + async verifyNotPatternedKeys(delimiter: string): Promise { + const notPatternedKeys = Selector('[data-testid^="badge"]').parent('[data-testid^="node-item_"]'); + const notPatternedKeysNumber = await notPatternedKeys.count; + + for (let i = 0; i < notPatternedKeysNumber; i++) { + await t.expect(notPatternedKeys.nth(i).withText(delimiter).exists).notOk('Not contained delimiter keys'); } - // Verify that every level of tree view is clickable + } + + /** + * Get not added in array folder selector + * @param array folders selectors + * @param folderName name of folder + * @param delimiter string with delimiter value + */ + getFolderSelector(array: string[], folderName: string, delimiter: string): string { + if (array.length === 0) { + return `[data-testid="node-item_${folderName}${delimiter}"]`; + } + + const lastSelector = array[array.length - 1].substring(0, array[array.length - 1].length - 2); + return `${lastSelector}${folderName}${delimiter}"]`; + } + + /** + * Check tree view structure + * @param folders name of folders for tree view build + * @param delimiter string with delimiter value + */ + async checkTreeViewFoldersStructure(folders: string[][], delimiter: string): Promise { + // Verify not patterned keys + await this.verifyNotPatternedKeys(delimiter); + const foldersNumber = folders.length; + for (let i = 0; i < foldersNumber; i++) { const innerFoldersNumber = folders[i].length; const array: string[] = []; + for (let j = 0; j < innerFoldersNumber; j++) { - if (j === 0) { - const folderSelector = `[data-testid="node-item_${folders[i][j]}${delimiter}"]`; - array.push(folderSelector); - await t.click(Selector(folderSelector)); - } - else { - const lastSelector = array[array.length - 1].substring(0, array[array.length - 1].length - 2); - const folderSelector = `${lastSelector}${folders[i][j]}${delimiter}"]`; - array.push(folderSelector); - await t.click(Selector(folderSelector)); - } + const folderSelector = this.getFolderSelector(array, folders[i][j], delimiter); + array.push(folderSelector); + await t.click(Selector(folderSelector)); } + // Verify that the last folder level contains required keys const foundKeyName = `${folders[i].join(delimiter)}`; await t diff --git a/tests/e2e/tests/web/critical-path/tree-view/delimiter.e2e.ts b/tests/e2e/tests/web/critical-path/tree-view/delimiter.e2e.ts index e96d37957f..db1e2ffa7c 100644 --- a/tests/e2e/tests/web/critical-path/tree-view/delimiter.e2e.ts +++ b/tests/e2e/tests/web/critical-path/tree-view/delimiter.e2e.ts @@ -37,5 +37,5 @@ test('Verify that user can see that input is not saved when the Cancel button is // Change delimiter await browserPage.TreeView.changeDelimiterInTreeView('-'); // Verify that when user changes the delimiter and clicks on Save button delimiter is applied - await browserActions.checkTreeViewFoldersStructure([['device_us', 'west'], ['mobile_eu', 'central'], ['mobile_us', 'east'], ['user_us', 'west'], ['device_eu', 'central'], ['user_eu', 'central']], '-', true); + await browserActions.checkTreeViewFoldersStructure([['device_us', 'west'], ['mobile_eu', 'central'], ['mobile_us', 'east'], ['user_us', 'west'], ['device_eu', 'central'], ['user_eu', 'central']], '-'); }); From 7e4a83917226238734728a6073861ce526542496 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Wed, 1 Nov 2023 11:33:18 +0100 Subject: [PATCH 12/12] fix by pr comments --- tests/e2e/common-actions/browser-actions.ts | 33 +++++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/tests/e2e/common-actions/browser-actions.ts b/tests/e2e/common-actions/browser-actions.ts index a9275cffba..836fa6235e 100644 --- a/tests/e2e/common-actions/browser-actions.ts +++ b/tests/e2e/common-actions/browser-actions.ts @@ -72,18 +72,22 @@ export class BrowserActions { } /** - * Get not added in array folder selector - * @param array folders selectors + * Get node name by folders + * @param startFolder start folder * @param folderName name of folder * @param delimiter string with delimiter value */ - getFolderSelector(array: string[], folderName: string, delimiter: string): string { - if (array.length === 0) { - return `[data-testid="node-item_${folderName}${delimiter}"]`; - } + getNodeName(startFolder: string, folderName: string, delimiter: string): string { + return startFolder + folderName + delimiter; + + } - const lastSelector = array[array.length - 1].substring(0, array[array.length - 1].length - 2); - return `${lastSelector}${folderName}${delimiter}"]`; + /** + * Get node selector by name + * @param name node name + */ + getNodeSelector(name: string): Selector { + return Selector(`[data-testid="node-item_${name}"]`); } /** @@ -99,19 +103,22 @@ export class BrowserActions { for (let i = 0; i < foldersNumber; i++) { const innerFoldersNumber = folders[i].length; - const array: string[] = []; + let prevNodeSelector = ''; for (let j = 0; j < innerFoldersNumber; j++) { - const folderSelector = this.getFolderSelector(array, folders[i][j], delimiter); - array.push(folderSelector); - await t.click(Selector(folderSelector)); + const nodeName = this.getNodeName(prevNodeSelector, folders[i][j], delimiter); + const node = this.getNodeSelector(nodeName); + await t.click(node); + prevNodeSelector = nodeName; } // Verify that the last folder level contains required keys const foundKeyName = `${folders[i].join(delimiter)}`; + const firstFolderName = this.getNodeName('', folders[i][0], delimiter); + const firstFolder = this.getNodeSelector(firstFolderName); await t .expect(Selector(`[data-testid*="node-item_${foundKeyName}"]`).find('[data-testid^="key-"]').exists).ok('Specific key not found') - .click(array[0]); + .click(firstFolder); } } }