diff --git a/redisinsight/ui/index.tsx b/redisinsight/ui/index.tsx index b51dda7731..0ed8fb6ef3 100644 --- a/redisinsight/ui/index.tsx +++ b/redisinsight/ui/index.tsx @@ -3,9 +3,11 @@ import { createRoot } from 'react-dom/client' import App from 'uiSrc/App' import Router from 'uiSrc/Router' import { listenPluginsEvents } from 'uiSrc/plugins/pluginEvents' +import { migrateLocalStorageData } from 'uiSrc/services' import 'uiSrc/styles/base/_fonts.scss' import 'uiSrc/styles/main.scss' +migrateLocalStorageData() listenPluginsEvents() const rootEl = document.getElementById('root') diff --git a/redisinsight/ui/indexElectron.tsx b/redisinsight/ui/indexElectron.tsx index 641a01f9a4..f18b813246 100644 --- a/redisinsight/ui/indexElectron.tsx +++ b/redisinsight/ui/indexElectron.tsx @@ -2,12 +2,14 @@ import React from 'react' import { createRoot } from 'react-dom/client' import AppElectron from 'uiSrc/electron/AppElectron' import { listenPluginsEvents } from 'uiSrc/plugins/pluginEvents' +import { migrateLocalStorageData } from 'uiSrc/services' import 'uiSrc/styles/base/_fonts.scss' import 'uiSrc/styles/main.scss' window.app.sendWindowId((_e: any, windowId: string = '') => { window.windowId = windowId || window.windowId + migrateLocalStorageData() listenPluginsEvents() const rootEl = document.getElementById('root') diff --git a/redisinsight/ui/src/constants/browser.ts b/redisinsight/ui/src/constants/browser.ts index 453f28dd97..ab4b077c6a 100644 --- a/redisinsight/ui/src/constants/browser.ts +++ b/redisinsight/ui/src/constants/browser.ts @@ -1,6 +1,7 @@ +import { EuiComboBoxOptionOption } from '@elastic/eui' import { KeyValueFormat, SortOrder } from './keys' -export const DEFAULT_DELIMITER = ':' +export const DEFAULT_DELIMITER: EuiComboBoxOptionOption = { label: ':' } export const DEFAULT_TREE_SORTING = SortOrder.ASC export const DEFAULT_SHOW_HIDDEN_RECOMMENDATIONS = false diff --git a/redisinsight/ui/src/helpers/constructKeysToTree.ts b/redisinsight/ui/src/helpers/constructKeysToTree.ts index 52cb959f6d..67c8a5bcca 100644 --- a/redisinsight/ui/src/helpers/constructKeysToTree.ts +++ b/redisinsight/ui/src/helpers/constructKeysToTree.ts @@ -3,20 +3,21 @@ import { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys' interface Props { items: IKeyPropTypes[] - delimiter?: string + delimiterPattern?: string + delimiters?: string[] sorting?: SortOrder } export const constructKeysToTree = (props: Props): any[] => { - const { items: keys, delimiter = ':', sorting = 'ASC' } = props - const keysSymbol = `keys${delimiter}keys` + const { items: keys, delimiterPattern = ':', delimiters = [], sorting = 'ASC' } = props + const keysSymbol = `keys${delimiterPattern}keys` const tree: any = {} keys.forEach((key: any) => { // eslint-disable-next-line prefer-object-spread let currentNode: any = tree const { nameString: name = '' } = key - const nameSplitted = name.split(delimiter) + const nameSplitted = name.split(new RegExp(delimiterPattern, 'g')) const lastIndex = nameSplitted.length - 1 nameSplitted.forEach((value:any, index: number) => { @@ -78,33 +79,34 @@ export const constructKeysToTree = (props: Props): any[] => { return treeNodes.map((key, index) => { const name = key?.toString() const node: any = { nameString: name } - const tillNowKeyName = previousKey + name + delimiter const path = prevIndex ? `${prevIndex}.${index}` : `${index}` // populate node with children nodes if (!tree[key].isLeaf && Object.keys(tree[key]).length > 0) { + const delimiterView = delimiters.length === 1 ? delimiters[0] : '-' node.children = formatTreeData( tree[key], - tillNowKeyName, + `${previousKey + name + delimiterView}`, delimiter, path, ) node.keyCount = node.children.reduce((a: any, b:any) => a + (b.keyCount || 1), 0) node.keyApproximate = (node.keyCount / keys.length) * 100 + node.fullName = previousKey + name } else { // populate leaf node.isLeaf = true node.children = [] node.nameString = name.slice(0, -keysSymbol.length) node.nameBuffer = tree[key]?.name + node.fullName = previousKey + name + delimiter } node.path = path - node.fullName = tillNowKeyName node.id = getUniqueId() return node }) } - return formatTreeData(tree, '', delimiter) + return formatTreeData(tree, '', delimiterPattern) } diff --git a/redisinsight/ui/src/helpers/tests/constructKeysToTree.spec.ts b/redisinsight/ui/src/helpers/tests/constructKeysToTree.spec.ts index c2c413f1e2..88e846dbca 100644 --- a/redisinsight/ui/src/helpers/tests/constructKeysToTree.spec.ts +++ b/redisinsight/ui/src/helpers/tests/constructKeysToTree.spec.ts @@ -1,12 +1,10 @@ -import { DEFAULT_DELIMITER } from 'uiSrc/constants' -import { constructKeysToTreeMockResult } from './constructKeysToTreeMockResult' +import { constructKeysToTreeMockResult, delimiterMock } from './constructKeysToTreeMockResult' import { constructKeysToTree } from '../constructKeysToTree' const constructKeysToTreeTests: any[] = [ [{ items: [ { nameString: 'keys:1:2', type: 'hash', ttl: -1, size: 71 }, - { nameString: 'keys2', type: 'hash', ttl: -1, size: 71 }, { nameString: 'keys:1:1', type: 'hash', ttl: -1, size: 71 }, { nameString: 'empty::test', type: 'hash', ttl: -1, size: 71 }, { nameString: 'test1', type: 'hash', ttl: -1, size: 71 }, @@ -15,8 +13,9 @@ const constructKeysToTreeTests: any[] = [ { nameString: 'keys1', type: 'hash', ttl: -1, size: 71 }, { nameString: 'keys:3', type: 'hash', ttl: -1, size: 71 }, { nameString: 'keys:2', type: 'hash', ttl: -1, size: 71 }, + { nameString: 'keys_2', type: 'hash', ttl: -1, size: 71 }, ], - delimiter: DEFAULT_DELIMITER + delimiterPattern: delimiterMock }, constructKeysToTreeMockResult ] diff --git a/redisinsight/ui/src/helpers/tests/constructKeysToTreeMockResult.ts b/redisinsight/ui/src/helpers/tests/constructKeysToTreeMockResult.ts index 0bce8a266c..7dcd4dc32c 100644 --- a/redisinsight/ui/src/helpers/tests/constructKeysToTreeMockResult.ts +++ b/redisinsight/ui/src/helpers/tests/constructKeysToTreeMockResult.ts @@ -1,3 +1,4 @@ +export const delimiterMock = ':|_' export const constructKeysToTreeMockResult = [ { nameString: 'empty', @@ -10,19 +11,19 @@ export const constructKeysToTreeMockResult = [ isLeaf: true, children: [], path: '0.0.0', - fullName: 'empty::empty::testkeys:keys:', + fullName: `empty--empty::testkeys${delimiterMock}keys${delimiterMock}`, } ], keyCount: 1, keyApproximate: 10, path: '0.0', - fullName: 'empty::', + fullName: 'empty-', } ], keyCount: 1, keyApproximate: 10, path: '0', - fullName: 'empty:', + fullName: 'empty', }, { nameString: 'keys', @@ -35,74 +36,74 @@ export const constructKeysToTreeMockResult = [ isLeaf: true, children: [], path: '1.0.0', - fullName: 'keys:1:keys:1:1keys:keys:', + fullName: `keys-1-keys:1:1keys${delimiterMock}keys${delimiterMock}`, }, { nameString: 'keys:1:2', isLeaf: true, children: [], path: '1.0.1', - fullName: 'keys:1:keys:1:2keys:keys:', + fullName: `keys-1-keys:1:2keys${delimiterMock}keys${delimiterMock}`, } ], keyCount: 2, keyApproximate: 20, path: '1.0', - fullName: 'keys:1:', + fullName: 'keys-1', }, { - nameString: 'keys:1', + nameString: 'keys_2', isLeaf: true, children: [], path: '1.1', - fullName: 'keys:keys:1keys:keys:', + fullName: `keys-keys_2keys${delimiterMock}keys${delimiterMock}`, }, { - nameString: 'keys:2', + nameString: 'keys:1', isLeaf: true, children: [], path: '1.2', - fullName: 'keys:keys:2keys:keys:', + fullName: `keys-keys:1keys${delimiterMock}keys${delimiterMock}`, }, { - nameString: 'keys:3', + nameString: 'keys:2', isLeaf: true, children: [], path: '1.3', - fullName: 'keys:keys:3keys:keys:', + fullName: `keys-keys:2keys${delimiterMock}keys${delimiterMock}`, + }, + { + nameString: 'keys:3', + isLeaf: true, + children: [], + path: '1.4', + fullName: `keys-keys:3keys${delimiterMock}keys${delimiterMock}`, } ], - keyCount: 5, - keyApproximate: 50, + keyCount: 6, + keyApproximate: 60, path: '1', - fullName: 'keys:', + fullName: 'keys', }, { nameString: 'keys1', isLeaf: true, children: [], path: '2', - fullName: 'keys1keys:keys:', - }, - { - nameString: 'keys2', - isLeaf: true, - children: [], - path: '3', - fullName: 'keys2keys:keys:', + fullName: `keys1keys${delimiterMock}keys${delimiterMock}`, }, { nameString: 'test1', isLeaf: true, children: [], - path: '4', - fullName: 'test1keys:keys:', + path: '3', + fullName: `test1keys${delimiterMock}keys${delimiterMock}`, }, { nameString: 'test2', isLeaf: true, children: [], - path: '5', - fullName: 'test2keys:keys:', + path: '4', + fullName: `test2keys${delimiterMock}keys${delimiterMock}`, } ] 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 ab955fb949..61da55dbd3 100644 --- a/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx @@ -279,7 +279,7 @@ const KeyList = forwardRef((props: Props, ref) => { minWidth: 94, truncateText: true, render: (cellData: string) => ( - + ) }, { 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 index 8ee2f086ea..6e2a14ecc4 100644 --- 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 @@ -13,15 +13,15 @@ describe('KeyRowName', () => { expect(render()).toBeTruthy() }) - it('should render Loading if no nameString', () => { - const { queryByTestId } = render() + it('should render Loading if no nameString and shortName', () => { + 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() + 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 index 5dfc1df372..3e18f9bd87 100644 --- a/redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.tsx @@ -11,12 +11,13 @@ import styles from './styles.module.scss' export interface Props { nameString: Maybe + shortName: Maybe } const KeyRowName = (props: Props) => { - const { nameString } = props + const { nameString, shortName } = props - if (isUndefined(nameString)) { + if (isUndefined(shortName)) { return ( { } // Better to cut the long string, because it could affect virtual scroll performance - const nameContent = replaceSpaces(nameString?.substring?.(0, 200)) + const nameContent = replaceSpaces(shortName?.substring?.(0, 200)) const nameTooltipContent = formatLongName(nameString) return (
-
+
{ const { instanceId } = useParams<{ instanceId: string }>() const { openNodes } = useSelector(appContextBrowserTree) - const { treeViewDelimiter: delimiter = '', treeViewSort: sorting } = useSelector(appContextDbConfig) + const { treeViewDelimiter, treeViewSort: sorting } = useSelector(appContextDbConfig) const { nameString: selectedKeyName = null } = useSelector(selectedKeyDataSelector) ?? {} const [statusOpen, setStatusOpen] = useState(openNodes) @@ -67,6 +68,12 @@ const KeyTree = forwardRef((props: Props, ref) => { const [firstDataLoaded, setFirstDataLoaded] = useState(!!keysState.keys.length) const [items, setItems] = useState(parseKeyNames(keysState.keys ?? [])) + // escape regexp symbols and join and transform to regexp + const delimiters = comboBoxToArray(treeViewDelimiter) + const delimiterPattern = delimiters + .map(escapeRegExp) + .join('|') + const dispatch = useDispatch() useImperativeHandle(ref, () => ({ @@ -86,8 +93,8 @@ const KeyTree = forwardRef((props: Props, ref) => { // 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) + const parts = selectedKeyName.split(delimiterPattern) + const parents = parts.map((_, index) => parts.slice(0, index + 1).join(delimiterPattern) + delimiterPattern) // remove key name from parents parents.pop() @@ -110,7 +117,7 @@ const KeyTree = forwardRef((props: Props, ref) => { } setItems(parseKeyNames(keysState.keys)) - }, [keysState.lastRefreshTime, delimiter, sorting]) + }, [keysState.lastRefreshTime, delimiterPattern, sorting]) useEffect(() => { openSelectedKey(selectedKeyName) @@ -188,7 +195,8 @@ const KeyTree = forwardRef((props: Props, ref) => { () @@ -23,7 +24,6 @@ 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(() => { @@ -63,7 +63,10 @@ describe('KeyTreeDelimiter', () => { }) await waitForEuiPopoverVisible() - expect(screen.getByTestId(DELIMITER_INPUT)).toBeInTheDocument() + const comboboxInput = document + .querySelector('[data-testid="delimiter-combobox"] [data-test-subj="comboBoxSearchInput"]') as HTMLInputElement + + expect(comboboxInput).toBeInTheDocument() expect(screen.getByTestId(SORTING_SELECT)).toBeInTheDocument() }) @@ -79,7 +82,21 @@ describe('KeyTreeDelimiter', () => { await waitForEuiPopoverVisible() - fireEvent.change(screen.getByTestId(DELIMITER_INPUT), { target: { value } }) + const comboboxInput = document + .querySelector('[data-testid="delimiter-combobox"] [data-test-subj="comboBoxSearchInput"]') as HTMLInputElement + + fireEvent.change( + comboboxInput, + { target: { value } } + ) + + fireEvent.keyDown(comboboxInput, { key: 'Enter', code: 13, charCode: 13 }) + + const containerLabels = document.querySelector('[data-test-subj="comboBoxInput"]')! + expect(containerLabels.querySelector(`[title="${value}"]`)).toBeInTheDocument() + + fireEvent.click(containerLabels.querySelector('[title^="Remove :"]')!) + expect(containerLabels.querySelector('[title=":"]')).not.toBeInTheDocument() await act(() => { fireEvent.click(screen.getByTestId(SORTING_SELECT)) @@ -89,14 +106,16 @@ describe('KeyTreeDelimiter', () => { await act(() => { fireEvent.click(screen.getByTestId(SORTING_DESC_ITEM)) - }) + }); + + (sendEventTelemetry as jest.Mock).mockRestore() await act(() => { fireEvent.click(screen.getByTestId(APPLY_BTN)) }) const expectedActions = [ - setBrowserTreeDelimiter(value), + setBrowserTreeDelimiter([{ label: value }]), resetBrowserTree(), setBrowserTreeSort(SortOrder.DESC), resetBrowserTree(), @@ -110,8 +129,8 @@ describe('KeyTreeDelimiter', () => { event: TelemetryEvent.TREE_VIEW_DELIMITER_CHANGED, eventData: { databaseId: INSTANCE_ID_MOCK, - from: DEFAULT_DELIMITER, - to: value, + from: comboBoxToArray([DEFAULT_DELIMITER]), + to: [value], } }) @@ -127,7 +146,6 @@ describe('KeyTreeDelimiter', () => { }) it('"setBrowserTreeDelimiter" should be called with DEFAULT_DELIMITER after Apply change with empty input', async () => { - const value = '' render() await act(() => { @@ -136,14 +154,16 @@ describe('KeyTreeDelimiter', () => { await waitForEuiPopoverVisible() - fireEvent.change(screen.getByTestId(DELIMITER_INPUT), { target: { value } }) + const containerLabels = document.querySelector('[data-test-subj="comboBoxInput"]')! + fireEvent.click(containerLabels.querySelector('[title^="Remove :"]')!) + expect(containerLabels.querySelector('[title=":"]')).not.toBeInTheDocument() await act(() => { fireEvent.click(screen.getByTestId(APPLY_BTN)) }) const expectedActions = [ - setBrowserTreeDelimiter(DEFAULT_DELIMITER), + setBrowserTreeDelimiter([DEFAULT_DELIMITER]), resetBrowserTree(), ] 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 78bfce85e3..58a000e626 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 @@ -2,7 +2,19 @@ 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 { + EuiButton, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPopover, + EuiSuperSelect, + EuiText, + EuiComboBox, + EuiComboBoxOptionOption, +} from '@elastic/eui' +import { isEqual } from 'lodash' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { DEFAULT_DELIMITER, DEFAULT_TREE_SORTING, SortOrder } from 'uiSrc/constants' @@ -13,13 +25,13 @@ import { setBrowserTreeSort, } from 'uiSrc/slices/app/context' import TreeViewSort from 'uiSrc/assets/img/browser/treeViewSort.svg?react' +import { comboBoxToArray } from 'uiSrc/utils' 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: ( @@ -29,9 +41,12 @@ const sortOptions = [SortOrder.ASC, SortOrder.DESC].map((value) => ({ const KeyTreeSettings = ({ loading }: Props) => { const { instanceId = '' } = useParams<{ instanceId: string }>() - const { treeViewDelimiter = '', treeViewSort = DEFAULT_TREE_SORTING } = useSelector(appContextDbConfig) + const { + treeViewDelimiter = [DEFAULT_DELIMITER], + treeViewSort = DEFAULT_TREE_SORTING, + } = useSelector(appContextDbConfig) const [sorting, setSorting] = useState(treeViewSort) - const [delimiter, setDelimiter] = useState(treeViewDelimiter) + const [delimiters, setDelimiters] = useState(treeViewDelimiter) const [isPopoverOpen, setIsPopoverOpen] = useState(false) @@ -42,7 +57,7 @@ const KeyTreeSettings = ({ loading }: Props) => { }, [treeViewSort]) useEffect(() => { - setDelimiter(treeViewDelimiter) + setDelimiters(treeViewDelimiter) }, [treeViewDelimiter]) const onButtonClick = () => setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen) @@ -55,7 +70,7 @@ const KeyTreeSettings = ({ loading }: Props) => { const resetStates = useCallback(() => { setSorting(treeViewSort) - setDelimiter(treeViewDelimiter) + setDelimiters(treeViewDelimiter) }, [treeViewSort, treeViewDelimiter]) const button = ( @@ -70,14 +85,16 @@ const KeyTreeSettings = ({ loading }: Props) => { ) const handleApply = () => { - if (delimiter !== treeViewDelimiter) { - dispatch(setBrowserTreeDelimiter(delimiter || DEFAULT_DELIMITER)) + if (!isEqual(delimiters, treeViewDelimiter)) { + const delimitersValue = delimiters.length ? delimiters : [DEFAULT_DELIMITER] + + dispatch(setBrowserTreeDelimiter(delimitersValue)) sendEventTelemetry({ event: TelemetryEvent.TREE_VIEW_DELIMITER_CHANGED, eventData: { databaseId: instanceId, - from: treeViewDelimiter, - to: delimiter || DEFAULT_DELIMITER + from: comboBoxToArray(treeViewDelimiter), + to: comboBoxToArray(delimitersValue) } }) @@ -122,14 +139,16 @@ const KeyTreeSettings = ({ loading }: Props) => {
Delimiter
- setDelimiter(e.target.value)} - aria-label="Title" - maxLength={MAX_DELIMITER_LENGTH} - data-testid="tree-view-delimiter-input" + delimiter=" " + selectedOptions={delimiters} + onCreateOption={(del) => setDelimiters([...delimiters, { label: del }])} + onChange={(selectedOptions) => setDelimiters(selectedOptions)} + className={styles.combobox} + data-testid="delimiter-combobox" />
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 9118c31517..6d730a7be9 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 @@ -2,7 +2,8 @@ margin-left: 8px; margin-top: 1px; - :global(.euiPopover), .anchorWrapper { + :global(.euiPopover), + .anchorWrapper { height: 100%; } } @@ -18,7 +19,6 @@ } .popoverWrapper { - height: 162px; width: 300px; padding: 14px 16px !important; border: none !important; @@ -32,17 +32,18 @@ } } .euiFormControlLayout { - height: 26px !important; + min-height: 26px !important; + height: auto !important; width: auto; } } - .input, .select { + .select { width: 188px; height: 24px !important; font-size: 12px; border-radius: 4px; - background-color: var(--browserViewTypePassive) !important; + background-color: var(--euiColorEmptyShade) !important; } } @@ -50,7 +51,7 @@ display: flex; width: 66px; font-size: 12px; - color: var(--euiTextSubduedColor)!important; + color: var(--euiTextSubduedColor) !important; } .title { @@ -82,3 +83,23 @@ margin-left: 8px; } } + +.combobox { + padding-bottom: 6px; + + :global(.euiComboBox__input) { + height: 30px !important; + } + :global(.euiComboBox__inputWrap) { + max-height: 200px; + overflow: auto; + max-width: 210px !important; + padding: 2px 4px !important; + min-height: 34px !important; + border-radius: 4px !important; + } + :global(.euiComboBoxPlaceholder) { + height: 30px !important; + line-height: 30px; + } +} 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 b4f5947fbb..b31520116c 100644 --- a/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.tsx +++ b/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.tsx @@ -13,12 +13,12 @@ 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, DEFAULT_TREE_SORTING, KeyTypes, ModulesKeyTypes, SortOrder, Theme } from 'uiSrc/constants' +import { 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 { RedisResponseBuffer, RedisString } from 'uiSrc/slices/interfaces' import { fetchKeysMetadataTree } from 'uiSrc/slices/browser/keys' -import { GetKeyInfoResponse } from 'apiSrc/modules/browser/dto' +import { GetKeyInfoResponse } from 'apiSrc/modules/browser/keys/dto' import { Node } from './components/Node' import { NodeMeta, TreeData, TreeNode } from './interfaces' @@ -27,7 +27,8 @@ import styles from './styles.module.scss' export interface Props { items: IKeyPropTypes[] - delimiter?: string + delimiterPattern: string + delimiters: string[] loadingIcon?: string loading: boolean deleting: boolean @@ -52,7 +53,8 @@ export const KEYS = 'keys' const VirtualTree = (props: Props) => { const { items, - delimiter = DEFAULT_DELIMITER, + delimiterPattern, + delimiters, loadingIcon = 'empty', statusOpen = {}, statusSelected, @@ -103,13 +105,13 @@ const VirtualTree = (props: Props) => { nodes.current = [] elements.current = {} rerender({}) - runWebworker?.({ items: [], delimiter, sorting }) + runWebworker?.({ items: [], delimiterPattern, delimiters, sorting }) return } setConstructingTree(true) - runWebworker?.({ items, delimiter, sorting }) - }, [items, delimiter]) + runWebworker?.({ items, delimiterPattern, delimiters, sorting }) + }, [items, delimiterPattern]) const handleUpdateSelected = useCallback((name: RedisString) => { onStatusSelected?.(name) @@ -154,7 +156,7 @@ const VirtualTree = (props: Props) => { ) => { const items = loadedItems.map(formatItem) - items.forEach((item) => updateNodeByPath(item.path, item)) + items.forEach((item: any) => updateNodeByPath(item.path, item)) rerender({}) } @@ -190,7 +192,8 @@ const VirtualTree = (props: Props) => { size: node.size, type: node.type, fullName: node.fullName, - shortName: node.nameString?.split(delimiter).pop(), + shortName: node.nameString?.split(new RegExp(delimiterPattern, 'g')).pop(), + delimiters, 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 597fe86359..15f96473ad 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 @@ -47,6 +47,7 @@ const Node = ({ nameString, keyApproximate, isSelected, + delimiters = [], getMetadata, onDelete, onDeleteClicked, @@ -54,6 +55,8 @@ const Node = ({ updateStatusSelected, } = data + const delimiterView = delimiters.length === 1 ? delimiters[0] : '-' + const [deletePopoverId, setDeletePopoverId] = useState>(undefined) useEffect(() => { @@ -127,7 +130,7 @@ const Node = ({ const Leaf = () => ( <> - + - {`${fullName}*`} -
+
+ {`${fullName + delimiterView}*`} + {delimiters.length > 1 && ( + + {delimiters.map((delimiter) => ( + {delimiter} + ))} + + )} +
{`${keyCount} key(s) (${Math.round(keyApproximate * 100) / 100}%)`} ) 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 dd3be8344d..f9093b0fa6 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 @@ -140,3 +140,29 @@ .deletePopover { max-width: 400px !important; } + +.folderTooltipHeader { + display: flex; + flex-wrap: wrap; + align-items: center; + word-break: break-all; +} + +.delimiters { + display: inline-flex; + flex-wrap: wrap; +} + +.folderPattern { + font-weight: bold; + margin-right: 4px; + white-space: normal; +} + +.delimiter { + margin-bottom: 2px; + padding: 2px 5px; + margin-right: 4px; + border-radius: 2px; + background-color: var(--euiColorLightestShade); +} 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 119c843d29..0f54655e27 100644 --- a/redisinsight/ui/src/pages/browser/components/virtual-tree/interfaces.ts +++ b/redisinsight/ui/src/pages/browser/components/virtual-tree/interfaces.ts @@ -50,6 +50,7 @@ export interface TreeData extends FixedSizeNodeData { nestingLevel: number deleting: boolean isSelected: boolean + delimiters: string[] children?: TreeData[] updateStatusOpen: (fullName: string, value: boolean) => void updateStatusSelected: (key: RedisString) => void diff --git a/redisinsight/ui/src/services/index.ts b/redisinsight/ui/src/services/index.ts index a35cf5a229..432c379246 100644 --- a/redisinsight/ui/src/services/index.ts +++ b/redisinsight/ui/src/services/index.ts @@ -1,5 +1,6 @@ /* eslint-disable import/first */ export * from './storage' +export * from './migrateStorageData' import apiService from './apiService' import resourcesService from './resourcesService' diff --git a/redisinsight/ui/src/services/migrateStorageData.ts b/redisinsight/ui/src/services/migrateStorageData.ts new file mode 100644 index 0000000000..9a950e4e69 --- /dev/null +++ b/redisinsight/ui/src/services/migrateStorageData.ts @@ -0,0 +1,26 @@ +import { isString } from 'lodash' +import { BrowserStorageItem } from 'uiSrc/constants' +import { getDBConfigStorageField, localStorageService, setDBConfigStorageField } from './storage' + +export const migrateLocalStorageData = () => { + migrateDelimiterTreeView() +} + +const migrateDelimiterTreeView = () => { + const prefix = 'dbConfig_' + const storage = localStorageService.getAll() + + // Iterate over all keys and filter for the dbConfig_ prefix + Object.keys(storage).forEach((key) => { + if (key.startsWith(prefix)) { + const instanceId = key.replace(prefix, '') + + const treeViewDelimiter = getDBConfigStorageField(instanceId, BrowserStorageItem.treeViewDelimiter) + + // Check if treeViewDelimiter is a string and needs transform to array + if (isString(treeViewDelimiter)) { + setDBConfigStorageField(instanceId, BrowserStorageItem.treeViewDelimiter, [{ label: treeViewDelimiter }]) + } + } + }) +} diff --git a/redisinsight/ui/src/slices/app/context.ts b/redisinsight/ui/src/slices/app/context.ts index 4421886dff..842a5e4fb5 100644 --- a/redisinsight/ui/src/slices/app/context.ts +++ b/redisinsight/ui/src/slices/app/context.ts @@ -1,4 +1,5 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { EuiComboBoxOptionOption } from '@elastic/eui' import { RelativeWidthSizes } from 'uiSrc/components/virtual-table/interfaces' import { CapabilityStorageItem, ConfigDBStorageItem } from 'uiSrc/constants/storage' import { Maybe, Nullable } from 'uiSrc/utils' @@ -37,7 +38,7 @@ export const initialState: StateAppContext = { contextRdiInstanceId: '', lastPage: '', dbConfig: { - treeViewDelimiter: DEFAULT_DELIMITER, + treeViewDelimiter: [DEFAULT_DELIMITER], treeViewSort: DEFAULT_TREE_SORTING, slowLogDurationUnit: DEFAULT_SLOWLOG_DURATION_UNIT, showHiddenRecommendations: DEFAULT_SHOW_HIDDEN_RECOMMENDATIONS, @@ -56,7 +57,6 @@ export const initialState: StateAppContext = { }, panelSizes: {}, tree: { - delimiter: DEFAULT_DELIMITER, openNodes: {}, selectedLeaf: null, }, @@ -127,7 +127,7 @@ const appContextSlice = createSlice({ state.workspace = payload || AppWorkspace.Databases }, setDbConfig: (state, { payload }) => { - state.dbConfig.treeViewDelimiter = payload?.treeViewDelimiter ?? DEFAULT_DELIMITER + 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 @@ -136,8 +136,8 @@ const appContextSlice = createSlice({ state.dbConfig.slowLogDurationUnit = payload setDBConfigStorageField(state.contextInstanceId, ConfigDBStorageItem.slowLogDurationUnit, payload) }, - setBrowserTreeDelimiter: (state, { payload }: { payload: string }) => { - state.dbConfig.treeViewDelimiter = payload + setBrowserTreeDelimiter: (state, { payload }: { payload: EuiComboBoxOptionOption[] }) => { + state.dbConfig.treeViewDelimiter = payload as any setDBConfigStorageField(state.contextInstanceId, BrowserStorageItem.treeViewDelimiter, payload) }, setBrowserTreeSort: (state, { payload }: PayloadAction) => { diff --git a/redisinsight/ui/src/slices/interfaces/app.ts b/redisinsight/ui/src/slices/interfaces/app.ts index 994b3c41e2..f1ca5bec56 100644 --- a/redisinsight/ui/src/slices/interfaces/app.ts +++ b/redisinsight/ui/src/slices/interfaces/app.ts @@ -1,4 +1,5 @@ import { AxiosError } from 'axios' +import { EuiComboBoxOptionOption } from '@elastic/eui' import { RelativeWidthSizes } from 'uiSrc/components/virtual-table/interfaces' import { Nullable } from 'uiSrc/utils' import { DurationUnits, FeatureFlags, ICommands, SortOrder } from 'uiSrc/constants' @@ -64,7 +65,7 @@ export interface StateAppContext { contextRdiInstanceId: string lastPage: string dbConfig: { - treeViewDelimiter: string + treeViewDelimiter: EuiComboBoxOptionOption[] treeViewSort: SortOrder slowLogDurationUnit: DurationUnits showHiddenRecommendations: boolean @@ -85,7 +86,6 @@ export interface StateAppContext { [key: string]: number } tree: { - delimiter: string openNodes: { [key: string]: boolean } diff --git a/redisinsight/ui/src/styles/base/_inputs.scss b/redisinsight/ui/src/styles/base/_inputs.scss index ab4bd8d69b..df1938840e 100644 --- a/redisinsight/ui/src/styles/base/_inputs.scss +++ b/redisinsight/ui/src/styles/base/_inputs.scss @@ -51,3 +51,42 @@ input[name='sshPassphrase'] ~ .euiFormControlLayoutIcons { width: 34px !important; height: 29px !important; } + +.euiComboBox.euiComboBox-isOpen .euiComboBox__inputWrap { + background-image: none !important; + border-bottom: solid 2px var(--euiColorPrimary) !important; +} +.euiComboBox .euiComboBox__inputWrap { + .euiComboBoxPill { + background-color: var(--buttonDarkenBgColor) !important; + } + .euiBadge__iconButton { + margin-top: 1px; + margin-left: 8px; + margin-right: 3px; + border-radius: 50%; + background-color: var(--euiColorPrimary); + color: var(--rdiSecondaryBgColor); + width: 13px; + height: 13px; + + &:hover { + transform: translateY(-1px); + } + &:active { + transform: translateY(1px); + } + + svg { + width: 10px; + height: 10px; + + &:focus { + background: inherit; + } + } + } + .euiComboBox__input > input { + color: var(--inputTextColor) !important; + } +} diff --git a/redisinsight/ui/src/utils/tests/transformers/browser.spec.ts b/redisinsight/ui/src/utils/tests/transformers/browser.spec.ts new file mode 100644 index 0000000000..ebe90f832a --- /dev/null +++ b/redisinsight/ui/src/utils/tests/transformers/browser.spec.ts @@ -0,0 +1,16 @@ +import { comboBoxToArray } from 'uiSrc/utils' + +const getOutputForFormatToTextTests: any[] = [ + [[], []], + [[{ label: '123' }, { label: 'test' }], ['123', 'test']], + [[{ label1: '123' }], []], + [[{ label: '123' }, { label: 'test' }], ['123', 'test']], +] + +describe('formatToText', () => { + it.each(getOutputForFormatToTextTests)('for input: %s (reply), should be output: %s', + (reply, expected) => { + const result = comboBoxToArray(reply) + expect(result).toEqual(expected) + }) +}) diff --git a/redisinsight/ui/src/utils/transformers/browser.ts b/redisinsight/ui/src/utils/transformers/browser.ts new file mode 100644 index 0000000000..586a8da02f --- /dev/null +++ b/redisinsight/ui/src/utils/transformers/browser.ts @@ -0,0 +1,3 @@ +import { EuiComboBoxOptionOption } from '@elastic/eui' + +export const comboBoxToArray = (items: EuiComboBoxOptionOption[]) => [...items].map(({ label }) => label) diff --git a/redisinsight/ui/src/utils/transformers/index.ts b/redisinsight/ui/src/utils/transformers/index.ts index acbecc0489..013d674df3 100644 --- a/redisinsight/ui/src/utils/transformers/index.ts +++ b/redisinsight/ui/src/utils/transformers/index.ts @@ -11,6 +11,7 @@ export * from './extrapolation' export * from './transformQueryParams' export * from './getTruncatedName' export * from './transformRdiPipeline' +export * from './browser' export { replaceSpaces, diff --git a/tests/e2e/common-actions/browser-actions.ts b/tests/e2e/common-actions/browser-actions.ts index a778c03e31..a3ebb2a5bc 100644 --- a/tests/e2e/common-actions/browser-actions.ts +++ b/tests/e2e/common-actions/browser-actions.ts @@ -70,10 +70,10 @@ export class BrowserActions { } /** - * Verify that not patterned keys not visible with delimiter + * Verify that not patterned keys not displayed with delimiter * @param delimiter string with delimiter value */ - async verifyNotPatternedKeys(delimiter: string): Promise { + async verifyNotPatternedKeysNotDisplayed(delimiter: string): Promise { const notPatternedKeys = Selector('[data-testid^="badge"]').parent('[data-testid^="node-item_"]'); const notPatternedKeysNumber = await notPatternedKeys.count; @@ -88,9 +88,8 @@ export class BrowserActions { * @param folderName name of folder * @param delimiter string with delimiter value */ - getNodeName(startFolder: string, folderName: string, delimiter: string): string { - return startFolder + folderName + delimiter; - + getNodeName(startFolder: string, folderName: string, delimiter?: string): string { + return delimiter ? `${startFolder}${delimiter}${folderName}` : `${startFolder}${folderName}`; } /** @@ -106,29 +105,31 @@ export class BrowserActions { * @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; + async checkTreeViewFoldersStructure(folders: string[][], delimiters: string[]): Promise { + await this.verifyNotPatternedKeysNotDisplayed(delimiters[0]); - for (let i = 0; i < foldersNumber; i++) { - const innerFoldersNumber = folders[i].length; - let prevNodeSelector = ''; + for (let i = 0; i < folders.length; i++) { + const delimiter = delimiters.length > 1 ? '-' : delimiters[0]; + let prevNodeName = ''; + let prevDelimiter = ''; - for (let j = 0; j < innerFoldersNumber; j++) { - const nodeName = this.getNodeName(prevNodeSelector, folders[i][j], delimiter); + // Expand subfolders + for (let j = 0; j < folders[i].length; j++) { + const nodeName = this.getNodeName(prevNodeName, folders[i][j], prevDelimiter); const node = this.getNodeSelector(nodeName); const fullTestIdSelector = await node.getAttribute('data-testid'); + if (!fullTestIdSelector?.includes('expanded')) { await t.click(node); } - prevNodeSelector = nodeName; + + prevNodeName = nodeName; + prevDelimiter = delimiter; } // Verify that the last folder level contains required keys const foundKeyName = `${folders[i].join(delimiter)}`; - const firstFolderName = this.getNodeName('', folders[i][0], delimiter); + const firstFolderName = this.getNodeName('', folders[i][0]); const firstFolder = this.getNodeSelector(firstFolderName); await t .expect(Selector(`[data-testid*="node-item_${foundKeyName}"]`).find('[data-testid^="key-"]').exists).ok('Specific key not found') diff --git a/tests/e2e/pageObjects/components/browser/tree-view.ts b/tests/e2e/pageObjects/components/browser/tree-view.ts index 1a5c7d473c..709623152d 100644 --- a/tests/e2e/pageObjects/components/browser/tree-view.ts +++ b/tests/e2e/pageObjects/components/browser/tree-view.ts @@ -1,7 +1,10 @@ import { Selector, t } from 'testcafe'; import { Common } from '../../../helpers/common'; +import { FiltersDialog } from '../../dialogs'; export class TreeView { + FiltersDialog = new FiltersDialog(); + //------------------------------------------------------------------------------------------- //DECLARATION OF SELECTORS //*Declare all elements/components of the relevant page. @@ -10,17 +13,10 @@ export class TreeView { //------------------------------------------------------------------------------------------- //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]'); - 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 @@ -51,15 +47,16 @@ export class TreeView { /** * Change delimiter value - * @delimiter string with delimiter value + * @param delimiter string with delimiter value */ async changeDelimiterInTreeView(delimiter: string): Promise { // Open delimiter popup await t.click(this.treeViewSettingsBtn); + await this.FiltersDialog.clearDelimiterCombobox(); // Apply new value to the field - await t.typeText(this.treeViewDelimiterInput, delimiter, { replace: true, paste: true }); + await this.FiltersDialog.addDelimiterItem(delimiter); // Click on save button - await t.click(this.treeViewDelimiterValueSave); + await t.click(this.FiltersDialog.treeViewDelimiterValueSave); } /** @@ -69,13 +66,13 @@ export class TreeView { async changeOrderingInTreeView(order: string): Promise { // Open settings popup await t.click(this.treeViewSettingsBtn); - await t.click(this.sortingBtn); + await t.click(this.FiltersDialog.sortingBtn); order === 'ASC' - ? await t.click(this.sortingASCoption) - : await t.click(this.sortingDESCoption); + ? await t.click(this.FiltersDialog.sortingASCoption) + : await t.click(this.FiltersDialog.sortingDESCoption); // Click on save button - await t.click(this.treeViewDelimiterValueSave); + await t.click(this.FiltersDialog.treeViewDelimiterValueSave); await Common.waitForElementNotVisible(this.sortingProgressBar); } diff --git a/tests/e2e/pageObjects/dialogs/filters-dialog.ts b/tests/e2e/pageObjects/dialogs/filters-dialog.ts new file mode 100644 index 0000000000..f640a60ad8 --- /dev/null +++ b/tests/e2e/pageObjects/dialogs/filters-dialog.ts @@ -0,0 +1,57 @@ +import { Selector, t } from 'testcafe'; + +export class FiltersDialog { + // INPUTS + delimiterCombobox = Selector('[data-testid=delimiter-combobox]'); + delimiterComboboxInput = Selector('[data-test-subj=comboBoxSearchInput]'); + // BUTTONS + treeViewDelimiterValueCancel = Selector('[data-testid=tree-view-cancel-btn]'); + treeViewDelimiterValueSave = Selector('[data-testid=tree-view-apply-btn]'); + sortingBtn = Selector('[data-testid=tree-view-sorting-select]'); + sortingASCoption = Selector('[id=ASC]'); + sortingDESCoption = Selector('[id=DESC]'); + + /** + * Get Delimiter badge selector by title + * @param delimiterTitle title of the delimiter item + */ + getDelimiterBadgeByTitle(delimiterTitle: string): Selector { + return this.delimiterCombobox.find(`span[title='${delimiterTitle}']`); + } + + /** + * Get Delimiter close button selector by title + * @param delimiterTitle title of the delimiter item + */ + getDelimiterCloseBtnByTitle(delimiterTitle: string): Selector { + return this.getDelimiterBadgeByTitle(delimiterTitle).find('button'); + } + + /** + * Add new delimiter + * @param delimiterName name of the delimiter item + */ + async addDelimiterItem(delimiterName: string): Promise { + await t.click(this.delimiterComboboxInput); + await t.typeText(this.delimiterComboboxInput, delimiterName, { paste: true }) + } + + /** + * Delete existing delimiter + * @param delimiterName name of the delimiter item + */ + async removeDelimiterItem(delimiterName: string): Promise { + await t.click(this.getDelimiterCloseBtnByTitle(delimiterName)); + } + + /** + * Remove all existing delimiters in combobox + */ + async clearDelimiterCombobox(): Promise { + const delimiters = this.delimiterCombobox.find('button'); + const count = await delimiters.count; + for (let i = 0; i < count; i++) { + await t.click(delimiters.nth(i)); + } + } +} diff --git a/tests/e2e/pageObjects/dialogs/index.ts b/tests/e2e/pageObjects/dialogs/index.ts index c96a11a7c2..73c4d96b82 100644 --- a/tests/e2e/pageObjects/dialogs/index.ts +++ b/tests/e2e/pageObjects/dialogs/index.ts @@ -1,7 +1,9 @@ import { OnboardingCardsDialog } from './onboarding-cards-dialog'; +import { FiltersDialog } from './filters-dialog'; import { UserAgreementDialog } from './user-agreement-dialog'; export { OnboardingCardsDialog, + FiltersDialog, UserAgreementDialog }; 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 db1e2ffa7c..78aee90bd8 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 @@ -1,22 +1,27 @@ import { BrowserPage } from '../../../../pageObjects'; -import { commonUrl, ossStandaloneBigConfig } from '../../../../helpers/conf'; +import { commonUrl, ossStandaloneBigConfig, ossStandaloneV8Config } 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'; +import { APIKeyRequests } from '../../../../helpers/api/api-keys'; +import { HashKeyParameters } from '../../../../pageObjects/browser-page'; const browserPage = new BrowserPage(); const browserActions = new BrowserActions(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const apiKeyRequests = new APIKeyRequests(); + +let keyNames: string[]; fixture `Delimiter tests` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) - .beforeEach(async() => { + .beforeEach(async () => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); }) - .afterEach(async() => { + .afterEach(async () => { await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); }); test('Verify that user can see that input is not saved when the Cancel button is clicked', async t => { @@ -24,18 +29,72 @@ test('Verify that user can see that input is not saved when the Cancel button is await t.click(browserPage.treeViewButton); await t.click(browserPage.TreeView.treeViewSettingsBtn); // Check the default delimiter value - await t.expect(browserPage.TreeView.treeViewDelimiterInput.value).eql(':', 'Default delimiter not applied'); + await t.expect(browserPage.TreeView.FiltersDialog.getDelimiterBadgeByTitle(':').exists).eql(true, 'Default delimiter not applied'); // Apply new value to the field - await t.typeText(browserPage.TreeView.treeViewDelimiterInput, 'test', { replace: true }); + await browserPage.TreeView.FiltersDialog.removeDelimiterItem(':'); + await browserPage.TreeView.FiltersDialog.addDelimiterItem('test'); // Click on Cancel button - await t.click(browserPage.TreeView.treeViewDelimiterValueCancel); + await t.click(browserPage.TreeView.FiltersDialog.treeViewDelimiterValueCancel); // Check the previous delimiter value await t.click(browserPage.TreeView.treeViewSettingsBtn); - await t.expect(browserPage.TreeView.treeViewDelimiterInput.value).eql(':', 'Previous delimiter not applied'); - await t.click(browserPage.TreeView.treeViewDelimiterValueCancel); + await t.expect(browserPage.TreeView.FiltersDialog.getDelimiterBadgeByTitle(':').exists).eql(true, 'Previous delimiter not applied'); + await t.expect(browserPage.TreeView.FiltersDialog.getDelimiterBadgeByTitle('test').exists).eql(false, 'Previous delimiter not applied'); + await t.click(browserPage.TreeView.FiltersDialog.treeViewDelimiterValueCancel); // 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']], '-'); + await browserActions.checkTreeViewFoldersStructure([['device_us', 'west'], ['mobile_eu', 'central'], ['mobile_us', 'east'], ['user_us', 'west'], ['device_eu', 'central'], ['user_eu', 'central']], ['-']); }); +test + .before(async () => { + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV8Config); + keyNames = [ + `device:common-dev`, + `device-common:dev`, + `device:common:dev`, + `device-common-dev`, + `device:common-stage`, + `device:common1-stage`, + `mobile:common-dev`, + `mobile:common-stage` + ]; + for (const keyName of keyNames) { + let hashKeyParameters: HashKeyParameters = { + keyName: keyName, + fields: [ + { + field: 'field', + value: 'value', + }, + ], + } + await apiKeyRequests.addHashKeyApi( + hashKeyParameters, + ossStandaloneV8Config, + ) + } + await browserPage.reloadPage(); + }) + .after(async () => { + for (const keyName of keyNames) { + await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneV8Config.databaseName); + } + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneV8Config); + })('Verify that user can set multiple delimiters in the tree view', async t => { + // Switch to tree view + await t.click(browserPage.treeViewButton); + // Verify folders ordering with default delimiter + await browserActions.checkTreeViewFoldersStructure([['device', 'common'], ['device-common'], ['mobile']], [':']); + await t.click(browserPage.TreeView.treeViewSettingsBtn); + // Apply new value to the field + await browserPage.TreeView.FiltersDialog.addDelimiterItem('-'); + await t.click(browserPage.TreeView.FiltersDialog.treeViewDelimiterValueSave); + // Verify that when user changes the delimiter and clicks on Save button delimiter is applied + await browserActions.checkTreeViewFoldersStructure([['device', 'common'], ['device', 'common1'], ['mobile', 'common']], [':', '-']); + + // Verify that namespace names tooltip contains valid names and delimiter + await t.click(browserActions.getNodeSelector('device')); + await t.hover(browserActions.getNodeSelector('device-common')); + await browserActions.verifyTooltipContainsText('device-common-*\n:\n-\n5 key(s)', true); + }); 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 08be4a8c69..56c8249460 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 @@ -133,7 +133,7 @@ test('Verify that when user deletes the key he can see the key is removed from t await t.click(browserPage.treeViewButton); // Verify the default separator await t.click(browserPage.TreeView.treeViewSettingsBtn); - await t.expect(browserPage.TreeView.treeViewDelimiterInput.value).eql(':', 'The “:” (colon) not used as a default separator for namespaces'); + await t.expect(browserPage.TreeView.FiltersDialog.getDelimiterBadgeByTitle(':').exists).eql(true, '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.TreeView.treeViewKeysNumber.visible).ok('The user can not see the number of keys');