diff --git a/redisinsight/ui/src/constants/storage.ts b/redisinsight/ui/src/constants/storage.ts index 5d79cd8afc..e0c696b677 100644 --- a/redisinsight/ui/src/constants/storage.ts +++ b/redisinsight/ui/src/constants/storage.ts @@ -3,6 +3,7 @@ enum BrowserStorageItem { instancesSorting = 'instancesSorting', theme = 'theme', browserViewType = 'browserViewType', + browserSearchMode = 'browserSearchMode', cliClientUuid = 'cliClientUuid', cliResizableContainer = 'cliResizableContainer', cliInputHistory = 'cliInputHistory', 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 1ad1e1e108..73d87fee15 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 @@ -52,8 +52,10 @@ const BrowserLeftPanel = (props: Props) => { const redisearchKeysState = useSelector(redisearchDataSelector) const { loading: redisearchLoading, isSearched: redisearchIsSearched } = useSelector(redisearchSelector) const { loading: patternLoading, viewType, searchMode, isSearched: patternIsSearched } = useSelector(keysSelector) - const { keyList: { isDataLoaded } } = useSelector(appContextBrowser) const { contextInstanceId } = useSelector(appContextSelector) + const { + keyList: { isDataLoaded, scrollPatternTopPosition, scrollRedisearchTopPosition } + } = useSelector(appContextBrowser) const keyListRef = useRef() @@ -62,6 +64,7 @@ const BrowserLeftPanel = (props: Props) => { const keysState = searchMode === SearchMode.Pattern ? patternKeysState : redisearchKeysState const loading = searchMode === SearchMode.Pattern ? patternLoading : redisearchLoading const isSearched = searchMode === SearchMode.Pattern ? patternIsSearched : redisearchIsSearched + const scrollTopPosition = searchMode === SearchMode.Pattern ? scrollPatternTopPosition : scrollRedisearchTopPosition useEffect(() => { if (!isDataLoaded || contextInstanceId !== instanceId) { @@ -118,6 +121,7 @@ const BrowserLeftPanel = (props: Props) => { ref={keyListRef} keysState={keysState} loading={loading} + scrollTopPosition={scrollTopPosition} loadMoreItems={loadMoreItems} selectKey={selectKey} /> 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 f2b092da24..5f7573ca5c 100644 --- a/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx @@ -39,8 +39,9 @@ import { } from 'uiSrc/slices/browser/keys' import { appContextBrowser, - setBrowserKeyListScrollPosition, + setBrowserPatternScrollPosition, setBrowserIsNotRendered, + setBrowserRedisearchScrollPosition, } from 'uiSrc/slices/app/context' import { GroupBadge } from 'uiSrc/components' import { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api' @@ -59,6 +60,7 @@ export interface Props { hideHeader?: boolean keysState: KeysStoreData loading: boolean + scrollTopPosition: number hideFooter?: boolean selectKey: ({ rowData }: { rowData: any }) => void loadMoreItems?: ( @@ -69,7 +71,7 @@ export interface Props { const KeyList = forwardRef((props: Props, ref) => { let wheelTimer = 0 - const { selectKey, loadMoreItems, loading, keysState, hideFooter } = props + const { selectKey, loadMoreItems, loading, keysState, scrollTopPosition, hideFooter } = props const { instanceId = '' } = useParams<{ instanceId: string }>() @@ -77,7 +79,7 @@ const KeyList = forwardRef((props: Props, ref) => { const { total, nextCursor, previousResultCount } = useSelector(keysDataSelector) const { isSearched, isFiltered, viewType, searchMode } = useSelector(keysSelector) const { selectedIndex } = useSelector(redisearchSelector) - const { keyList: { scrollTopPosition, isNotRendered: isNotRenderedContext } } = useSelector(appContextBrowser) + const { keyList: { isNotRendered: isNotRenderedContext } } = useSelector(appContextBrowser) const [, rerender] = useState({}) const [firstDataLoaded, setFirstDataLoaded] = useState(!!keysState.keys.length) @@ -197,9 +199,13 @@ const KeyList = forwardRef((props: Props, ref) => { } } - const setScrollTopPosition = (position: number) => { - dispatch(setBrowserKeyListScrollPosition(position)) - } + const setScrollTopPosition = useCallback((position: number) => { + if (searchMode === SearchMode.Pattern) { + dispatch(setBrowserPatternScrollPosition(position)) + } else { + dispatch(setBrowserRedisearchScrollPosition(position)) + } + }, [searchMode]) const formatItem = useCallback((item: GetKeyInfoResponse): GetKeyInfoResponse => ({ ...item, @@ -388,32 +394,37 @@ const KeyList = forwardRef((props: Props, ref) => { }, ] + const VirtualizeTable = () => ( + + onRowsRenderedDebounced(overscanStartIndex, overscanStopIndex)} + /> + ) + return (
- - onRowsRenderedDebounced(overscanStartIndex, overscanStopIndex)} - /> + {searchMode === SearchMode.Pattern && VirtualizeTable()} + {searchMode !== SearchMode.Pattern && VirtualizeTable()}
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 74ed005e53..6217408f74 100644 --- a/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx +++ b/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx @@ -17,11 +17,11 @@ import { resetBrowserTree, setBrowserKeyListDataLoaded, } from 'uiSrc/slices/app import { changeKeyViewType, changeSearchMode, fetchKeys, keysSelector, resetKeysData, } from 'uiSrc/slices/browser/keys' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' -import { REDISEARCH_MODULES } from 'uiSrc/slices/interfaces' import { KeysStoreData, KeyViewType, SearchMode } from 'uiSrc/slices/interfaces/keys' import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import AutoRefresh from '../auto-refresh' +import { isRedisearchAvailable } from 'uiSrc/utils' +import AutoRefresh from '../auto-refresh' import FilterKeyType from '../filter-key-type' import RediSearchIndexesList from '../redisearch-key-list' import SearchKeyList from '../search-key-list' @@ -129,8 +129,7 @@ const KeysHeader = (props: Props) => { tooltipText: 'Search by Values of Keys', ariaLabel: 'Search by Values of Keys button', dataTestId: 'search-mode-redisearch-btn', - disabled: !modules?.some(({ name }) => - REDISEARCH_MODULES.some((search) => name === search)), + disabled: !isRedisearchAvailable(modules), isActiveView() { return searchMode === this.type }, getClassName() { return cx(styles.viewTypeBtn, { [styles.active]: this.isActiveView() }) @@ -255,6 +254,8 @@ const KeysHeader = (props: Props) => { } dispatch(changeSearchMode(mode)) + + localStorageService.set(BrowserStorageItem.browserSearchMode, mode) } const AddKeyBtn = ( diff --git a/redisinsight/ui/src/pages/browser/components/redisearch-key-list/RediSearchIndexesList.spec.tsx b/redisinsight/ui/src/pages/browser/components/redisearch-key-list/RediSearchIndexesList.spec.tsx index d23c703f5a..5548701239 100644 --- a/redisinsight/ui/src/pages/browser/components/redisearch-key-list/RediSearchIndexesList.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/redisearch-key-list/RediSearchIndexesList.spec.tsx @@ -13,6 +13,12 @@ import { } from 'uiSrc/utils/test-utils' import { loadKeys, loadList, redisearchListSelector, setSelectedIndex } from 'uiSrc/slices/browser/redisearch' import { bufferToString, stringToBuffer } from 'uiSrc/utils' +import { localStorageService } from 'uiSrc/services' +import { SearchMode } from 'uiSrc/slices/interfaces/keys' +import { RedisDefaultModules } from 'uiSrc/slices/interfaces' +import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { changeSearchMode, fetchKeys } from 'uiSrc/slices/browser/keys' +import { BrowserStorageItem } from 'uiSrc/constants' import RediSearchIndexesList, { Props } from './RediSearchIndexesList' let store: typeof mockedStore @@ -31,6 +37,7 @@ jest.mock('react-redux', () => ({ jest.mock('uiSrc/slices/browser/keys', () => ({ ...jest.requireActual('uiSrc/slices/browser/keys'), + fetchKeys: jest.fn(), keysSelector: jest.fn().mockReturnValue({ searchMode: 'Redisearch', }), @@ -45,6 +52,23 @@ jest.mock('uiSrc/slices/browser/redisearch', () => ({ }), })) +jest.mock('uiSrc/slices/instances/instances', () => ({ + ...jest.requireActual('uiSrc/slices/instances/instances'), + connectedInstanceSelector: jest.fn().mockReturnValue({ + id: '123', + connectionType: 'STANDALONE', + db: 0, + }), +})) + +jest.mock('uiSrc/services', () => ({ + ...jest.requireActual('uiSrc/services'), + localStorageService: { + set: jest.fn(), + get: jest.fn(), + }, +})) + describe('RediSearchIndexesList', () => { beforeEach(() => { const state: any = store.getState(); @@ -55,10 +79,25 @@ describe('RediSearchIndexesList', () => { ...state.browser, keys: { ...state.browser.keys, - searchMode: 'Redisearch', + searchMode: SearchMode.Redisearch, }, redisearch: { ...state.browser.redisearch, loading: false } + }, + connections: { + ...state.connections, + instances: { + ...state.connections.instances, + connectedInstance: { + ...state.connections.instances.connectedInstance, + modules: [{ name: RedisDefaultModules.Search, }], + } + } } + })); + + (connectedInstanceSelector as jest.Mock).mockImplementation(() => ({ + ...state.connections.instances.connectedInstance, + loading: true, })) }) @@ -68,13 +107,45 @@ describe('RediSearchIndexesList', () => { expect(searchInput).toBeInTheDocument() }) + it('should render and call changeSearchMode if no RediSearch module', () => { + localStorageService.set = jest.fn(); + + (connectedInstanceSelector as jest.Mock).mockImplementation(() => ({ + loading: false, + modules: [], + })) + + expect(render()).toBeTruthy() + + const expectedActions = [ + changeSearchMode(SearchMode.Pattern), + changeSearchMode(SearchMode.Pattern), + ] + + expect(clearStoreActions(store.getActions())).toEqual( + clearStoreActions(expectedActions) + ) + + expect(localStorageService.set).toBeCalledWith( + BrowserStorageItem.browserSearchMode, + SearchMode.Pattern, + ) + }) + it('"loadList" should be called after render', () => { - render( + const { rerender } = render( - ) + ); + + (connectedInstanceSelector as jest.Mock).mockImplementation(() => ({ + loading: false, + modules: [{ name: RedisDefaultModules.Search, }] + })) + + rerender() const expectedActions = [ - loadList() + loadList(), ] expect(clearStoreActions(store.getActions())).toEqual( clearStoreActions(expectedActions) @@ -93,29 +164,40 @@ describe('RediSearchIndexesList', () => { expect(onCreateIndexMock).toBeCalled() }) - it('"setSelectedIndex" and "loadKeys" should be called after select Index', () => { - const index = stringToBuffer('idx'); + it('"setSelectedIndex" and "loadKeys" should be called after select Index', async () => { + const index = stringToBuffer('idx') + const fetchKeysMock = jest.fn(); + + (fetchKeys as jest.Mock).mockReturnValue(fetchKeysMock); (redisearchListSelector as jest.Mock).mockReturnValue({ data: [index], loading: false, error: '', + selectedIndex: null, }) const { queryByText } = render( - ) + ); + + (connectedInstanceSelector as jest.Mock).mockImplementation(() => ({ + loading: false, + modules: [{ name: RedisDefaultModules.Search, }] + })) fireEvent.click(screen.getByTestId('select-search-mode')) fireEvent.click(queryByText(bufferToString(index)) || document) const expectedActions = [ - loadList(), setSelectedIndex(index), - loadKeys(), + loadList(), ] + expect(clearStoreActions(store.getActions())).toEqual( clearStoreActions(expectedActions) ) + + expect(fetchKeysMock).toBeCalled() }) }) diff --git a/redisinsight/ui/src/pages/browser/components/redisearch-key-list/RediSearchIndexesList.tsx b/redisinsight/ui/src/pages/browser/components/redisearch-key-list/RediSearchIndexesList.tsx index 3fe56a1b9c..2fa6f1e523 100644 --- a/redisinsight/ui/src/pages/browser/components/redisearch-key-list/RediSearchIndexesList.tsx +++ b/redisinsight/ui/src/pages/browser/components/redisearch-key-list/RediSearchIndexesList.tsx @@ -16,11 +16,13 @@ import { fetchRedisearchListAction, } from 'uiSrc/slices/browser/redisearch' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' -import { KeyViewType } from 'uiSrc/slices/interfaces/keys' -import { fetchKeys, keysSelector } from 'uiSrc/slices/browser/keys' +import { KeyViewType, SearchMode } from 'uiSrc/slices/interfaces/keys' +import { changeSearchMode, fetchKeys, keysSelector } from 'uiSrc/slices/browser/keys' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { bufferToString, formatLongName, Nullable } from 'uiSrc/utils' +import { bufferToString, formatLongName, isRedisearchAvailable, Nullable } from 'uiSrc/utils' import { SCAN_COUNT_DEFAULT, SCAN_TREE_COUNT_DEFAULT } from 'uiSrc/constants/api' +import { localStorageService } from 'uiSrc/services' +import { BrowserStorageItem } from 'uiSrc/constants' import styles from './styles.module.scss' @@ -36,7 +38,7 @@ const RediSearchIndexesList = (props: Props) => { const { viewType, searchMode } = useSelector(keysSelector) const { selectedIndex = '' } = useSelector(redisearchSelector) const { data: list = [], loading } = useSelector(redisearchListSelector) - const { id: instanceId } = useSelector(connectedInstanceSelector) + const { id: instanceId, modules, loading: instanceLoading } = useSelector(connectedInstanceSelector) const [isSelectOpen, setIsSelectOpen] = useState(false) const [index, setIndex] = useState>(JSON.stringify(selectedIndex)) @@ -44,12 +46,21 @@ const RediSearchIndexesList = (props: Props) => { const dispatch = useDispatch() useEffect(() => { - setIndex(JSON.stringify(selectedIndex || '')) - }, [selectedIndex]) + if (instanceLoading) return + + const moduleExists = isRedisearchAvailable(modules) + if (moduleExists) { + dispatch(fetchRedisearchListAction()) + } else { + dispatch(changeSearchMode(SearchMode.Pattern)) + + localStorageService.set(BrowserStorageItem.browserSearchMode, SearchMode.Pattern) + } + }, [instanceLoading, modules]) useEffect(() => { - dispatch(fetchRedisearchListAction()) - }, []) + setIndex(JSON.stringify(selectedIndex || '')) + }, [selectedIndex]) const options: EuiSuperSelectOption[] = list.map( (index) => { diff --git a/redisinsight/ui/src/slices/app/context.ts b/redisinsight/ui/src/slices/app/context.ts index 8b07de2b50..819b11b19b 100644 --- a/redisinsight/ui/src/slices/app/context.ts +++ b/redisinsight/ui/src/slices/app/context.ts @@ -11,7 +11,8 @@ export const initialState: StateAppContext = { browser: { keyList: { isDataLoaded: false, - scrollTopPosition: 0, + scrollPatternTopPosition: 0, + scrollRedisearchTopPosition: 0, isNotRendered: true, selectedKey: null, }, @@ -65,8 +66,11 @@ const appContextSlice = createSlice({ setBrowserKeyListDataLoaded: (state, { payload }: { payload: boolean }) => { state.browser.keyList.isDataLoaded = payload }, - setBrowserKeyListScrollPosition: (state, { payload }: { payload: number }) => { - state.browser.keyList.scrollTopPosition = payload + setBrowserPatternScrollPosition: (state, { payload }: { payload: number }) => { + state.browser.keyList.scrollPatternTopPosition = payload + }, + setBrowserRedisearchScrollPosition: (state, { payload }: { payload: number }) => { + state.browser.keyList.scrollRedisearchTopPosition = payload }, setBrowserIsNotRendered: (state, { payload }: { payload: boolean }) => { state.browser.keyList.isNotRendered = payload @@ -156,7 +160,8 @@ export const { setAppContextConnectedInstanceId, setBrowserKeyListDataLoaded, setBrowserSelectedKey, - setBrowserKeyListScrollPosition, + setBrowserPatternScrollPosition, + setBrowserRedisearchScrollPosition, setBrowserIsNotRendered, setBrowserPanelSizes, setBrowserTreeSelectedLeaf, @@ -173,7 +178,7 @@ export const { setWorkbenchEAItemScrollTop, setPubSubFieldsContext, setBrowserBulkActionOpen, - setLastAnalyticsPage + setLastAnalyticsPage, } = appContextSlice.actions // Selectors diff --git a/redisinsight/ui/src/slices/browser/keys.ts b/redisinsight/ui/src/slices/browser/keys.ts index be94264d1b..6dc17253b7 100644 --- a/redisinsight/ui/src/slices/browser/keys.ts +++ b/redisinsight/ui/src/slices/browser/keys.ts @@ -12,7 +12,8 @@ import { isStatusSuccessful, Maybe, bufferToString, - isEqualBuffers + isEqualBuffers, + isRedisearchAvailable, } from 'uiSrc/utils' import { DEFAULT_SEARCH_MATCH, SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api' import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent, getAdditionalAddedEventData, getMatchType } from 'uiSrc/telemetry' @@ -62,7 +63,7 @@ export const initialState: KeysStore = { isSearched: false, isFiltered: false, isBrowserFullScreen: false, - searchMode: SearchMode.Pattern, + searchMode: localStorageService?.get(BrowserStorageItem.browserSearchMode) ?? SearchMode.Pattern, viewType: localStorageService?.get(BrowserStorageItem.browserViewType) ?? KeyViewType.Browser, data: { total: 0, @@ -313,6 +314,7 @@ const keysSlice = createSlice({ { ...initialState, viewType: localStorageService?.get(BrowserStorageItem.browserViewType) ?? KeyViewType.Browser, + searchMode: localStorageService?.get(BrowserStorageItem.browserSearchMode) ?? SearchMode.Pattern, selectedKey: getInitialSelectedKeyState(state as KeysStore) } ), @@ -923,9 +925,14 @@ export function fetchKeys( onSuccess?: () => void, onFailed?: () => void, ) { - return searchMode === SearchMode.Pattern - ? fetchPatternKeysAction(cursor, count, onSuccess, onFailed,) - : fetchRedisearchKeysAction(cursor, count, onSuccess, onFailed,) + return async (dispatch: AppDispatch, stateInit: () => RootState) => { + const state = stateInit() + const isRedisearchExists = isRedisearchAvailable(state.connections.instances.connectedInstance.modules) + + return searchMode === SearchMode.Pattern || !isRedisearchExists + ? dispatch(fetchPatternKeysAction(cursor, count, onSuccess, onFailed)) + : dispatch(fetchRedisearchKeysAction(cursor, count, onSuccess, onFailed)) + } } // Asynchronous thunk action diff --git a/redisinsight/ui/src/slices/instances/instances.ts b/redisinsight/ui/src/slices/instances/instances.ts index d61dc5febe..be33b45878 100644 --- a/redisinsight/ui/src/slices/instances/instances.ts +++ b/redisinsight/ui/src/slices/instances/instances.ts @@ -33,6 +33,7 @@ export const initialState: InitialStateInstances = { connectionType: ConnectionType.Standalone, isRediStack: false, modules: [], + loading: false, }, instanceOverview: { version: '', @@ -138,12 +139,22 @@ const instancesSlice = createSlice({ }, // set connected instance - setConnectedInstance: (state, { payload }: { payload: Instance }) => { + setConnectedInstance: (state) => { + state.connectedInstance.loading = true + }, + + // set connected instance success + setConnectedInstanceSuccess: (state, { payload }: { payload: Instance }) => { const isRediStack = state.data?.find((db) => db.id === state.connectedInstance.id)?.isRediStack state.connectedInstance = payload + state.connectedInstance.loading = false state.connectedInstance.isRediStack = isRediStack || false }, + setConnectedInstanceFailure: (state) => { + state.connectedInstance.loading = false + }, + // reset connected instance resetConnectedInstance: (state) => { state.connectedInstance = initialState.connectedInstance @@ -162,6 +173,8 @@ export const { setDefaultInstance, setDefaultInstanceSuccess, setDefaultInstanceFailure, + setConnectedInstanceSuccess, + setConnectedInstanceFailure, setConnectedInstance, setConnectedInstanceId, resetConnectedInstance, @@ -314,16 +327,18 @@ export function deleteInstancesAction(instances: Instance[], onSuccess?: () => v export function fetchInstanceAction(id: string, onSuccess?: () => void) { return async (dispatch: AppDispatch) => { dispatch(setDefaultInstance()) + dispatch(setConnectedInstance()) try { const { data, status } = await apiService.get(`${ApiEndpoints.INSTANCE}/${id}`) if (isStatusSuccessful(status)) { - dispatch(setConnectedInstance(data)) + dispatch(setConnectedInstanceSuccess(data)) } onSuccess?.() } catch (error) { const errorMessage = getApiErrorMessage(error) + dispatch(setConnectedInstanceFailure()) dispatch(setDefaultInstanceFailure(errorMessage)) dispatch(addErrorNotification(error)) } diff --git a/redisinsight/ui/src/slices/interfaces/app.ts b/redisinsight/ui/src/slices/interfaces/app.ts index 0b927e5ff4..308a5fb70b 100644 --- a/redisinsight/ui/src/slices/interfaces/app.ts +++ b/redisinsight/ui/src/slices/interfaces/app.ts @@ -40,7 +40,8 @@ export interface StateAppContext { browser: { keyList: { isDataLoaded: boolean - scrollTopPosition: number + scrollPatternTopPosition: number + scrollRedisearchTopPosition: number isNotRendered: boolean selectedKey: Nullable }, diff --git a/redisinsight/ui/src/slices/interfaces/instances.ts b/redisinsight/ui/src/slices/interfaces/instances.ts index 62fe52daea..76bb1e53d4 100644 --- a/redisinsight/ui/src/slices/interfaces/instances.ts +++ b/redisinsight/ui/src/slices/interfaces/instances.ts @@ -39,6 +39,7 @@ export interface Instance extends DatabaseInstanceResponse { modules: RedisModuleDto[] isRediStack?: boolean visible?: boolean + loading?: boolean } export enum ConnectionType { diff --git a/redisinsight/ui/src/slices/tests/app/context.spec.ts b/redisinsight/ui/src/slices/tests/app/context.spec.ts index 3518e277a2..29d241a020 100644 --- a/redisinsight/ui/src/slices/tests/app/context.spec.ts +++ b/redisinsight/ui/src/slices/tests/app/context.spec.ts @@ -13,7 +13,7 @@ import reducer, { setAppContextConnectedInstanceId, setBrowserKeyListDataLoaded, setBrowserSelectedKey, - setBrowserKeyListScrollPosition, + setBrowserPatternScrollPosition, setBrowserPanelSizes, setWorkbenchScript, setWorkbenchVerticalPanelSizes, @@ -33,6 +33,7 @@ import reducer, { updateBrowserTreeSelectedLeaf, setBrowserTreeDelimiter, setBrowserIsNotRendered, + setBrowserRedisearchScrollPosition, } from '../../app/context' jest.mock('uiSrc/services', () => ({ @@ -172,20 +173,47 @@ describe('slices', () => { }) }) - describe('setBrowserKeyListScrollPosition', () => { + describe('setBrowserPatternScrollPosition', () => { it('should properly set scroll position of keyList', () => { // Arrange - const scrollTopPosition = 530 + const scrollPatternTopPosition = 530 const state = { ...initialState.browser, keyList: { ...initialState.browser.keyList, - scrollTopPosition + scrollPatternTopPosition } } // Act - const nextState = reducer(initialState, setBrowserKeyListScrollPosition(scrollTopPosition)) + const nextState = reducer(initialState, setBrowserPatternScrollPosition(scrollPatternTopPosition)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + app: { context: nextState }, + }) + + expect(appContextBrowser(rootState)).toEqual(state) + }) + }) + + describe('setBrowserRedisearchScrollPosition', () => { + it('should properly set scroll position of keyList', () => { + // Arrange + const scrollRedisearchTopPosition = 530 + const state = { + ...initialState.browser, + keyList: { + ...initialState.browser.keyList, + scrollRedisearchTopPosition + } + } + + // Act + const nextState = reducer( + initialState, + setBrowserRedisearchScrollPosition(scrollRedisearchTopPosition) + ) // Assert const rootState = Object.assign(initialStateDefault, { diff --git a/redisinsight/ui/src/slices/tests/browser/redisearch.spec.ts b/redisinsight/ui/src/slices/tests/browser/redisearch.spec.ts index c5ef3868f9..9a2efd7585 100644 --- a/redisinsight/ui/src/slices/tests/browser/redisearch.spec.ts +++ b/redisinsight/ui/src/slices/tests/browser/redisearch.spec.ts @@ -2,12 +2,14 @@ import { AxiosError } from 'axios' import { cloneDeep, omit } from 'lodash' import successMessages from 'uiSrc/components/notifications/success-messages' import { apiService } from 'uiSrc/services' -import { cleanup, initialStateDefault, mockedStore } from 'uiSrc/utils/test-utils' +import { cleanup, initialStateDefault, mockedStore, mockStore } from 'uiSrc/utils/test-utils' import { addErrorNotification, addMessageNotification } from 'uiSrc/slices/app/notifications' import { stringToBuffer } from 'uiSrc/utils' import { REDISEARCH_LIST_DATA_MOCK } from 'uiSrc/mocks/handlers/browser/redisearchHandlers' import { SearchMode } from 'uiSrc/slices/interfaces/keys' import { fetchKeys, fetchMoreKeys } from 'uiSrc/slices/browser/keys' +import { initialState as initialStateInstances } from 'uiSrc/slices/instances/instances' +import { RedisDefaultModules } from 'uiSrc/slices/interfaces' import reducer, { initialState, loadKeys, @@ -713,15 +715,27 @@ describe('redisearch slice', () => { apiService.post = jest.fn().mockResolvedValue(responsePayload) + const newStore = mockStore({ + ...initialStateDefault, + connections: { + instances: { + ...cloneDeep(initialStateInstances), + connectedInstance: { + modules: [{ name: RedisDefaultModules.Search }] + } + } + } + }) + // Act - await store.dispatch(fetchKeys(SearchMode.Redisearch, '0', 20)) + await newStore.dispatch(fetchKeys(SearchMode.Redisearch, '0', 20)) // Assert const expectedActions = [ loadKeys(), loadKeysSuccess([data, false]), ] - expect(store.getActions()).toEqual(expectedActions) + expect(newStore.getActions()).toEqual(expectedActions) }) it('failed to load keys', async () => { @@ -736,8 +750,20 @@ describe('redisearch slice', () => { apiService.post = jest.fn().mockRejectedValue(responsePayload) + const newStore = mockStore({ + ...initialStateDefault, + connections: { + instances: { + ...cloneDeep(initialStateInstances), + connectedInstance: { + modules: [{ name: RedisDefaultModules.Search }] + } + } + } + }) + // Act - await store.dispatch(fetchKeys(SearchMode.Redisearch, '0', 20)) + await newStore.dispatch(fetchKeys(SearchMode.Redisearch, '0', 20)) // Assert const expectedActions = [ @@ -745,7 +771,7 @@ describe('redisearch slice', () => { addErrorNotification(responsePayload as AxiosError), loadKeysFailure(errorMessage), ] - expect(store.getActions()).toEqual(expectedActions) + expect(newStore.getActions()).toEqual(expectedActions) }) it('failed to load keys: Index not found', async () => { @@ -760,8 +786,20 @@ describe('redisearch slice', () => { apiService.post = jest.fn().mockRejectedValue(responsePayload) + const newStore = mockStore({ + ...initialStateDefault, + connections: { + instances: { + ...cloneDeep(initialStateInstances), + connectedInstance: { + modules: [{ name: RedisDefaultModules.Search }] + } + } + } + }) + // Act - await store.dispatch(fetchKeys(SearchMode.Redisearch, '0', 20)) + await newStore.dispatch(fetchKeys(SearchMode.Redisearch, '0', 20)) // Assert const expectedActions = [ @@ -771,7 +809,7 @@ describe('redisearch slice', () => { setRedisearchInitialState(), loadList(), ] - expect(store.getActions()).toEqual(expectedActions) + expect(newStore.getActions()).toEqual(expectedActions) }) }) diff --git a/redisinsight/ui/src/slices/tests/instances/instances.spec.ts b/redisinsight/ui/src/slices/tests/instances/instances.spec.ts index 6ed614404a..c2c3115153 100644 --- a/redisinsight/ui/src/slices/tests/instances/instances.spec.ts +++ b/redisinsight/ui/src/slices/tests/instances/instances.spec.ts @@ -37,6 +37,10 @@ import reducer, { changeInstanceAliasSuccess, changeInstanceAliasAction, resetConnectedInstance, + setConnectedInstanceId, + setConnectedInstance, + setConnectedInstanceFailure, + setConnectedInstanceSuccess, } from '../../instances/instances' import { addErrorNotification, addMessageNotification, IAddInstanceErrorPayload } from '../../app/notifications' import { ConnectionType, InitialStateInstances, Instance } from '../../interfaces' @@ -386,6 +390,101 @@ describe('instances slice', () => { }) }) + describe('setConnectedInstanceId', () => { + it('should properly set "id"', () => { + // Arrange + const id = 'id' + const state: InitialStateInstances = { + ...initialState, + connectedInstance: { + ...initialState.connectedInstance, + id, + } + } + + // Act + const nextState = reducer(initialState, setConnectedInstanceId(id)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + connections: { + instances: nextState, + }, + }) + expect(instancesSelector(rootState)).toEqual(state) + }) + }) + + describe('setConnectedInstance', () => { + it('should properly set loading = "true"', () => { + // Arrange + const state: InitialStateInstances = { + ...initialState, + connectedInstance: { + ...initialState.connectedInstance, + loading: true, + } + } + + // Act + const nextState = reducer(initialState, setConnectedInstance()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + connections: { + instances: nextState, + }, + }) + expect(instancesSelector(rootState)).toEqual(state) + }) + }) + + describe('setConnectedInstanceSuccess', () => { + it('should properly set error', () => { + // Arrange + const instance = { ...instances[1] } + const state: InitialStateInstances = { + ...initialState, + connectedInstance: instance + } + + // Act + const nextState = reducer(initialState, setConnectedInstanceSuccess(instance)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + connections: { + instances: nextState, + }, + }) + expect(instancesSelector(rootState)).toEqual(state) + }) + }) + + describe('setConnectedInstanceFailure', () => { + it('should properly set loading = "false"', () => { + // Arrange + const state = { + ...initialState, + connectedInstance: { + ...initialState.connectedInstance, + loading: false, + } + } + + // Act + const nextState = reducer(initialState, setConnectedInstanceFailure()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + connections: { + instances: nextState, + }, + }) + expect(instancesSelector(rootState)).toEqual(state) + }) + }) + describe('thunks', () => { describe('fetchInstances', () => { it('call both fetchInstances and loadInstancesSuccess when fetch is successed', async () => { diff --git a/redisinsight/ui/src/utils/index.ts b/redisinsight/ui/src/utils/index.ts index 082a77c42b..5b2ed6405b 100644 --- a/redisinsight/ui/src/utils/index.ts +++ b/redisinsight/ui/src/utils/index.ts @@ -25,6 +25,7 @@ export * from './tree' export * from './pubSubUtils' export * from './formatters' export * from './groupTypes' +export * from './modules' export { Maybe, diff --git a/redisinsight/ui/src/utils/modules.ts b/redisinsight/ui/src/utils/modules.ts index 2156b7a6dc..fde8e72eae 100644 --- a/redisinsight/ui/src/utils/modules.ts +++ b/redisinsight/ui/src/utils/modules.ts @@ -1,4 +1,4 @@ -import { DATABASE_LIST_MODULES_TEXT, RedisDefaultModules } from 'uiSrc/slices/interfaces' +import { DATABASE_LIST_MODULES_TEXT, RedisDefaultModules, REDISEARCH_MODULES } from 'uiSrc/slices/interfaces' import { RedisModuleDto } from 'apiSrc/modules/instances/dto/database-instance.dto' export interface IDatabaseModule { @@ -36,3 +36,7 @@ export const sortModulesByName = (modules: RedisModuleDto[]) => [...modules].sor if (PREDEFINED_MODULE_NAMES_ORDER.indexOf(b.name) === -1) return -1 return PREDEFINED_MODULE_NAMES_ORDER.indexOf(a.name) - PREDEFINED_MODULE_NAMES_ORDER.indexOf(b.name) }) + +export const isRedisearchAvailable = (modules: RedisModuleDto[]): boolean => + modules?.some(({ name }) => + REDISEARCH_MODULES.some((search) => name === search)) diff --git a/redisinsight/ui/src/utils/tests/modules.spec.ts b/redisinsight/ui/src/utils/tests/modules.spec.ts index ffea5d1319..6b36050236 100644 --- a/redisinsight/ui/src/utils/tests/modules.spec.ts +++ b/redisinsight/ui/src/utils/tests/modules.spec.ts @@ -1,4 +1,5 @@ -import { IDatabaseModule, sortModules } from 'uiSrc/utils/modules' +import { RedisDefaultModules } from 'uiSrc/slices/interfaces' +import { IDatabaseModule, isRedisearchAvailable, sortModules } from 'uiSrc/utils/modules' const modules1: IDatabaseModule[] = [ { moduleName: 'RedisJSON', abbreviation: 'RS' }, @@ -43,3 +44,22 @@ describe('sortModules', () => { expect(sortModules(modules2)).toEqual(result2) }) }) + +const nameToModule = (name:string) => ({ name }) + +const getOutputForRedisearchAvailable: any[] = [ + [['1', 'json'].map(nameToModule), false], + [['1', 'uoeuoeu ueaooe'].map(nameToModule), false], + [['1', 'json', RedisDefaultModules.Search].map(nameToModule), true], + [['1', 'json', RedisDefaultModules.SearchLight].map(nameToModule), true], + [['1', 'json', RedisDefaultModules.FT].map(nameToModule), true], + [['1', 'json', RedisDefaultModules.FTL].map(nameToModule), true], +] + +describe.only('isRedisearchAvailable', () => { + it.each(getOutputForRedisearchAvailable)('for input: %s (reply), should be output: %s', + (reply, expected) => { + const result = isRedisearchAvailable(reply) + expect(result).toBe(expected) + }) +}) diff --git a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts index 011a157982..37abc490df 100644 --- a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts @@ -1,5 +1,6 @@ +import { t } from 'testcafe'; import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; -import { BrowserPage, CliPage } from '../../../pageObjects'; +import { BrowserPage, CliPage, MyRedisDatabasePage } from '../../../pageObjects'; import { commonUrl, ossStandaloneBigConfig, @@ -14,13 +15,21 @@ import { verifyKeysDisplayedInTheList } from '../../../helpers/keys'; const browserPage = new BrowserPage(); const common = new Common(); const cliPage = new CliPage(); +const myRedisDatabasePage = new MyRedisDatabasePage(); const patternModeTooltipText = 'Filter by Key Name or Pattern'; const redisearchModeTooltipText = 'Search by Values of Keys'; const notSelectedIndexText = 'Select an index and enter a query to search per values of keys.'; +const searchPerValue = '(@name:"Hall School") | (@students:[500, 1000])'; let keyName = common.generateWord(10); let keyNames: string[]; let indexName = common.generateWord(5); +async function verifyContext(): Promise { + await t + .expect(browserPage.selectIndexDdn.withText(indexName).exists).ok('Index selection not saved') + .expect(browserPage.filterByPatterSearchInput.value).eql(searchPerValue, 'Search per Value not saved in input') + .expect(browserPage.keyNameFormDetails.withExactText(keyName).exists).ok('Key details not opened'); +} fixture `Search capabilities in Browser` .meta({ type: 'critical_path', rte: rte.standalone }) @@ -68,7 +77,7 @@ test await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyNames[0])).ok(`The key ${keyNames[0]} not found`); await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyNames[1])).notOk(`Invalid key ${keyNames[1]} is displayed after search`); // Verify that user can search by index plus multiple key values - await browserPage.searchByKeyName('(@name:"Hall School") | (@students:[500, 1000])'); + await browserPage.searchByKeyName(searchPerValue); await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyNames[0])).ok(`The first valid key ${keyNames[0]} not found`); await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyNames[2])).ok(`The second valid key ${keyNames[2]} not found`); await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyNames[1])).notOk(`Invalid key ${keyNames[1]} is displayed after search`); @@ -185,3 +194,42 @@ test await t.click(browserPage.selectIndexDdn); await browserPage.selectIndexByName(indexName); }); +test + .before(async() => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + }) + .after(async() => { + // Clear and delete database + await cliPage.sendCommandInCli(`FT.DROPINDEX ${indexName}`); + await deleteStandaloneDatabaseApi(ossStandaloneConfig); + })('Context for RediSearch capability', async t => { + keyName = common.generateWord(10); + indexName = `idx:${keyName}`; + const commands = [ + `HSET ${keyName} "name" "Hall School" "description" " Spanning 10 states" "class" "independent" "type" "traditional" "address_city" "London" "address_street" "Manor Street" "students" 342 "location" "51.445417, -0.258352"`, + `FT.CREATE ${indexName} ON HASH PREFIX 1 "${keyName}" SCHEMA name TEXT NOSTEM description TEXT class TAG type TAG SEPARATOR ";" address_city AS city TAG address_street AS address TEXT NOSTEM students NUMERIC SORTABLE location GEO` + ]; + + await cliPage.sendCommandsInCli(commands); + await t.click(browserPage.redisearchModeBtn); + await browserPage.selectIndexByName(indexName); + await browserPage.searchByKeyName(searchPerValue); + // Select key + await t.click(await browserPage.getKeySelectorByName(keyName)); + + // Verify that Redisearch context (inputs, key selected, scroll, key details) saved after switching between pages + await t + .click(myRedisDatabasePage.workbenchButton) + .click(myRedisDatabasePage.browserButton); + await verifyContext(); + + // Verify that Redisearch context saved when switching between browser/tree view + await t.click(browserPage.treeViewButton); + await verifyContext(); + await t.click(browserPage.browserViewButton); + await verifyContext(); + + // Verify that Search control opened after reloading page + await common.reloadPage(); + await t.expect(browserPage.keyListTable.textContent).contains(notSelectedIndexText, 'Search by Values of Keys section not opened'); + });