Skip to content

Commit a757f66

Browse files
authored
Merge pull request #2650 from RedisInsight/feature/RI-4711_exact-match
#RI-4711 - add exact match option
2 parents 6f232d8 + 491277c commit a757f66

File tree

10 files changed

+163
-10
lines changed

10 files changed

+163
-10
lines changed

redisinsight/ui/src/components/multi-search/MultiSearch.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export interface Props {
3232
onDelete: (ids: string[]) => void
3333
loading?: boolean
3434
}
35+
optionalButton?: React.ReactNode
3536
onChange: (value: string) => void
3637
onChangeOptions?: (options: string[]) => void
3738
onClear?: () => void
@@ -52,6 +53,7 @@ const MultiSearch = (props: Props) => {
5253
onChange,
5354
onKeyDown,
5455
onClear = () => {},
56+
optionalButton,
5557
className,
5658
compressed,
5759
...rest
@@ -249,6 +251,7 @@ const MultiSearch = (props: Props) => {
249251
/>
250252
</EuiToolTip>
251253
)}
254+
{optionalButton}
252255
{!!suggestionOptions?.length && (
253256
<EuiToolTip
254257
content={suggestions?.buttonTooltipTitle}

redisinsight/ui/src/constants/storage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ enum BrowserStorageItem {
44
theme = 'theme',
55
browserViewType = 'browserViewType',
66
browserSearchMode = 'browserSearchMode',
7+
browserExactMatch = 'browserExactMatch',
78
cliClientUuid = 'cliClientUuid',
89
cliResizableContainer = 'cliResizableContainer',
910
cliInputHistory = 'cliInputHistory',

redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.spec.tsx

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,18 @@ import {
1010
screen,
1111
act,
1212
} from 'uiSrc/utils/test-utils'
13-
import { keysSelector, loadKeys, loadSearchHistory, setFilter, setPatternSearchMatch } from 'uiSrc/slices/browser/keys'
13+
import {
14+
changeExactMatch,
15+
keysSelector,
16+
loadKeys,
17+
loadSearchHistory,
18+
setFilter,
19+
setPatternSearchMatch
20+
} from 'uiSrc/slices/browser/keys'
1421

1522
import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'
1623
import { KeyViewType, SearchMode } from 'uiSrc/slices/interfaces/keys'
24+
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
1725
import SearchKeyList from './SearchKeyList'
1826

1927
jest.mock('uiSrc/slices/browser/keys', () => ({
@@ -47,6 +55,11 @@ jest.mock('uiSrc/slices/browser/redisearch', () => ({
4755
}),
4856
}))
4957

58+
jest.mock('uiSrc/telemetry', () => ({
59+
...jest.requireActual('uiSrc/telemetry'),
60+
sendEventTelemetry: jest.fn(),
61+
}))
62+
5063
let store: typeof mockedStore
5164
beforeEach(() => {
5265
cleanup()
@@ -137,4 +150,51 @@ describe('SearchKeyList', () => {
137150
clearStoreActions([...afterRenderActions])
138151
)
139152
})
153+
154+
it('should change exact match after click on button', () => {
155+
keysSelector.mockImplementation(() => ({
156+
searchMode: SearchMode.Pattern,
157+
viewType: KeyViewType.Browser,
158+
isSearch: false,
159+
isFiltered: false,
160+
}))
161+
162+
render(<SearchKeyList />)
163+
164+
const afterRenderActions = [...store.getActions()]
165+
166+
fireEvent.click(screen.getByTestId('exact-match-button'))
167+
168+
expect(clearStoreActions(store.getActions())).toEqual(
169+
clearStoreActions([...afterRenderActions, changeExactMatch(true)])
170+
)
171+
})
172+
173+
it('should call proper telemetry after click exact match button', () => {
174+
keysSelector.mockImplementation(() => ({
175+
searchMode: SearchMode.Pattern,
176+
viewType: KeyViewType.Browser,
177+
isSearch: false,
178+
isFiltered: false,
179+
}))
180+
181+
const sendEventTelemetryMock = jest.fn();
182+
(sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock)
183+
184+
render(<SearchKeyList />)
185+
186+
fireEvent.click(screen.getByTestId('exact-match-button'))
187+
188+
expect(sendEventTelemetry).toBeCalledWith({
189+
event: TelemetryEvent.BROWSER_FILTER_PER_PATTERN_CLICKED,
190+
eventData: {
191+
databaseId: 'instanceId',
192+
current: 'Exact',
193+
previous: 'Pattern',
194+
view: KeyViewType.Browser
195+
}
196+
});
197+
198+
(sendEventTelemetry as jest.Mock).mockRestore()
199+
})
140200
})

redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.tsx

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import { keys } from '@elastic/eui'
1+
import { EuiButtonIcon, EuiToolTip, keys } from '@elastic/eui'
22
import React, { useEffect, useState } from 'react'
33
import { useDispatch, useSelector } from 'react-redux'
44
import cx from 'classnames'
55

6+
import { useParams } from 'react-router-dom'
67
import MultiSearch from 'uiSrc/components/multi-search/MultiSearch'
78
import { SCAN_COUNT_DEFAULT, SCAN_TREE_COUNT_DEFAULT } from 'uiSrc/constants/api'
89
import { replaceSpaces } from 'uiSrc/utils'
910
import {
11+
changeExactMatch,
1012
deleteSearchHistoryAction,
1113
fetchKeys,
1214
fetchSearchHistoryAction,
@@ -15,13 +17,11 @@ import {
1517
setFilter,
1618
setSearchMatch
1719
} from 'uiSrc/slices/browser/keys'
18-
import { SearchMode, KeyViewType, SearchHistoryItem } from 'uiSrc/slices/interfaces/keys'
19-
import {
20-
redisearchHistorySelector,
21-
redisearchSelector
22-
} from 'uiSrc/slices/browser/redisearch'
20+
import { KeyViewType, SearchHistoryItem, SearchMode } from 'uiSrc/slices/interfaces/keys'
21+
import { redisearchHistorySelector, redisearchSelector } from 'uiSrc/slices/browser/redisearch'
2322

2423
import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'
24+
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
2525
import styles from './styles.module.scss'
2626

2727
const placeholders = {
@@ -31,7 +31,7 @@ const placeholders = {
3131

3232
const SearchKeyList = () => {
3333
const { id } = useSelector(connectedInstanceSelector)
34-
const { search, viewType, searchMode } = useSelector(keysSelector)
34+
const { search, viewType, searchMode, exactMatch } = useSelector(keysSelector)
3535
const { search: redisearchQuery, selectedIndex } = useSelector(redisearchSelector)
3636
const { data: rediSearchHistory, loading: rediSearchHistoryLoading } = useSelector(redisearchHistorySelector)
3737
const { data: searchHistory, loading: searchHistoryLoading } = useSelector(keysSearchHistorySelector)
@@ -40,6 +40,7 @@ const SearchKeyList = () => {
4040
const [disableSubmit, setDisableSubmit] = useState(false)
4141

4242
const dispatch = useDispatch()
43+
const { instanceId } = useParams<{ instanceId: string }>()
4344

4445
useEffect(() => {
4546
if (id) {
@@ -114,6 +115,23 @@ const SearchKeyList = () => {
114115
handleApply('')
115116
}
116117

118+
const handleChangeExactMatch = () => {
119+
sendEventTelemetry({
120+
event: TelemetryEvent.BROWSER_FILTER_PER_PATTERN_CLICKED,
121+
eventData: {
122+
databaseId: instanceId,
123+
view: viewType,
124+
previous: exactMatch ? 'Exact' : 'Pattern',
125+
current: exactMatch ? 'Pattern' : 'Exact',
126+
}
127+
})
128+
dispatch(changeExactMatch(!exactMatch))
129+
130+
if (value) {
131+
handleApply()
132+
}
133+
}
134+
117135
return (
118136
<div className={cx(styles.container, { [styles.redisearchMode]: searchMode === SearchMode.Redisearch })}>
119137
<MultiSearch
@@ -130,6 +148,24 @@ const SearchKeyList = () => {
130148
onApply: handleApplySuggestion,
131149
onDelete: handleDeleteSuggestions,
132150
}}
151+
optionalButton={searchMode === SearchMode.Pattern ? (
152+
<EuiToolTip
153+
title="Exact Search"
154+
content={exactMatch ? 'Disable to see keys matching your pattern' : 'Enable to see keys that exactly match your pattern'}
155+
position="bottom"
156+
>
157+
<EuiButtonIcon
158+
display="empty"
159+
iconType="bullseye"
160+
color="primary"
161+
size="xs"
162+
onClick={handleChangeExactMatch}
163+
aria-label="exact match button"
164+
className={cx(styles.exactSearchIcon, { [styles.disabled]: !exactMatch })}
165+
data-testid="exact-match-button"
166+
/>
167+
</EuiToolTip>
168+
) : null}
133169
disableSubmit={disableSubmit}
134170
placeholder={placeholders[searchMode]}
135171
className={styles.input}

redisinsight/ui/src/pages/browser/components/search-key-list/styles.module.scss

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,12 @@
3131
margin-right: 50px;
3232
word-break: break-all;
3333
}
34+
35+
.exactSearchIcon {
36+
margin-left: 8px;
37+
margin-right: -4px;
38+
39+
&.disabled {
40+
opacity: 0.5;
41+
}
42+
}

redisinsight/ui/src/slices/browser/keys.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export const initialState: KeysStore = {
8080
isBrowserFullScreen: false,
8181
searchMode: localStorageService?.get(BrowserStorageItem.browserSearchMode) ?? SearchMode.Pattern,
8282
viewType: localStorageService?.get(BrowserStorageItem.browserViewType) ?? KeyViewType.Browser,
83+
exactMatch: localStorageService?.get(BrowserStorageItem.browserExactMatch) ?? true,
8384
data: {
8485
total: 0,
8586
scanned: 0,
@@ -355,6 +356,11 @@ const keysSlice = createSlice({
355356
state.searchMode = payload
356357
},
357358

359+
changeExactMatch: (state, { payload }: { payload: boolean }) => {
360+
state.exactMatch = payload
361+
localStorageService?.set(BrowserStorageItem.browserExactMatch, payload)
362+
},
363+
358364
resetAddKey: (state) => {
359365
state.addKey = cloneDeep(initialState.addKey)
360366
},
@@ -457,6 +463,7 @@ export const {
457463
toggleBrowserFullScreen,
458464
setViewFormat,
459465
changeSearchMode,
466+
changeExactMatch,
460467
loadSearchHistory,
461468
loadSearchHistorySuccess,
462469
loadSearchHistoryFailure,
@@ -513,16 +520,17 @@ export function fetchPatternKeysAction(
513520
sourceKeysFetch = CancelToken.source()
514521

515522
const state = stateInit()
516-
const { search: match, filter: type } = state.browser.keys
523+
const { search: match, filter: type, exactMatch } = state.browser.keys
517524
const { encoding } = state.app.info
525+
const withExactMatch = exactMatch ? match : `*${match}*`
518526

519527
const { data, status } = await apiService.post<GetKeysWithDetailsResponse[]>(
520528
getUrl(
521529
state.connections.instances?.connectedInstance?.id ?? '',
522530
ApiEndpoints.KEYS
523531
),
524532
{
525-
cursor, count, type, match: match || DEFAULT_SEARCH_MATCH, keysInfo: false,
533+
cursor, count, type, match: withExactMatch || DEFAULT_SEARCH_MATCH, keysInfo: false,
526534
},
527535
{
528536
params: { encoding },

redisinsight/ui/src/slices/interfaces/keys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export interface KeysStore {
3232
isBrowserFullScreen: boolean
3333
viewType: KeyViewType
3434
searchMode: SearchMode
35+
exactMatch: boolean
3536
data: KeysStoreData
3637
selectedKey: {
3738
loading: boolean

redisinsight/ui/src/telemetry/events.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export enum TelemetryEvent {
7575
BROWSER_WORKBENCH_LINK_CLICKED = 'BROWSER_WORKBENCH_LINK_CLICKED',
7676
BROWSER_DATABASE_INDEX_CHANGED = 'BROWSER_DATABASE_INDEX_CHANGED',
7777
BROWSER_FILTER_MODE_CHANGE_FAILED = 'BROWSER_FILTER_MODE_CHANGE_FAILED',
78+
BROWSER_FILTER_PER_PATTERN_CLICKED = 'BROWSER_FILTER_PER_PATTERN_CLICKED',
7879
BROWSER_TUTORIAL_CLICKED = 'BROWSER_TUTORIAL_CLICKED',
7980
LIST_VIEW_OPENED = 'LIST_VIEW_OPENED',
8081
OVERVIEW_MENU_CLICKED = 'OVERVIEW_MENU_CLICKED',

tests/e2e/pageObjects/browser-page.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export class BrowserPage extends InstancePage {
107107
clearFilterHistoryBtn = Selector('[data-testid=clear-history-btn]');
108108
guideLinksBtn = Selector('[data-testid^=guide-button-]');
109109
backToBrowserBtn = Selector('[data-testid=back-right-panel-btn]');
110+
exactSearchBtn = Selector('[data-testid=exact-match-button]');
110111
//CONTAINERS
111112
streamGroupsContainer = Selector('[data-testid=stream-groups-container]');
112113
streamConsumersContainer = Selector('[data-testid=stream-consumers-container]');

tests/e2e/tests/regression/browser/filtering.e2e.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import { DatabaseAPIRequests } from '../../../helpers/api/api-database';
77
import { keyTypes } from '../../../helpers/keys';
88
import { Common } from '../../../helpers/common';
99
import { APIKeyRequests } from '../../../helpers/api/api-keys';
10+
import { BrowserActions } from '../../../common-actions/browser-actions';
1011

1112
const browserPage = new BrowserPage();
1213
const databaseHelper = new DatabaseHelper();
1314
const databaseAPIRequests = new DatabaseAPIRequests();
1415
const apiKeyRequests = new APIKeyRequests();
16+
const browserActions = new BrowserActions();
1517

1618
let keyName = Common.generateWord(20);
1719
let keyName2 = Common.generateWord(20);
@@ -58,8 +60,39 @@ test('Verify that user can filter per pattern with ? (matches keys with any char
5860
// Filter per pattern with ?
5961
await browserPage.searchByKeyName(searchedValue);
6062
// Verify that key was found
63+
// Verify that filter requests are sent as entered when Exact Search enabled
6164
await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).ok('The key was not found');
6265
});
66+
test('Verify that filter requests are sent as entered when Exact Search enabled', async t => {
67+
const randomValue = Common.generateWord(10);
68+
const searchedValue = 'eyForSearch*?\\[]';
69+
const tooltipExactSearchEnabledText = 'Disable to see keys matching your pattern';
70+
const tooltipExactSearchDisabledText = 'Enable to see keys that exactly match your pattern';
71+
keyName = `KeyForSearch*?[]789${randomValue}`;
72+
73+
// Add new key
74+
await browserPage.addStringKey(keyName);
75+
// Filter by Exact Search
76+
await browserPage.searchByKeyName(searchedValue);
77+
// Verify that key was not found
78+
// Verify that Exact Search is enabled by default
79+
await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).notOk('The key was found by Exact Search with not full name');
80+
81+
// Verify that user can see ”Exact Search Disable to see keys matching your pattern” text when hover over the icon with enabled Exact Search
82+
await t.hover(browserPage.exactSearchBtn);
83+
await browserActions.verifyTooltipContainsText(tooltipExactSearchEnabledText, true);
84+
85+
// Disable Exact Search
86+
await t.click(browserPage.exactSearchBtn);
87+
// Verify that user can see ”Exact Search Enable to see keys that exactly match your pattern” text when hover over the icon with disabled Exact Search
88+
await t.hover(browserPage.exactSearchBtn);
89+
await browserActions.verifyTooltipContainsText(tooltipExactSearchDisabledText, true);
90+
91+
await browserPage.searchByKeyName(searchedValue);
92+
// Verify that key was found
93+
// Verify that user can see results that fit into this pattern: *{request}* when Exact Search is disabled
94+
await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).ok('The key was not found with disabled Exact Search');
95+
});
6396
test
6497
.after(async() => {
6598
// Clear and delete database

0 commit comments

Comments
 (0)