diff --git a/scripts/downloadBackend.ts b/scripts/downloadBackend.ts index 7550b5ee..0982f2ea 100755 --- a/scripts/downloadBackend.ts +++ b/scripts/downloadBackend.ts @@ -82,8 +82,8 @@ async function downloadRedisBackendArchive( }) } -function getNormalizedString(string: string) { - return string?.startsWith('D:') +function getNormalizedCIString(string: string) { + return string?.startsWith('D:') && process.env.CI ? upath.normalize(string).replace('D:', '/d') : string } @@ -96,9 +96,9 @@ function unzipRedisServer(redisInsideArchivePath: string, extractDir: string) { cp.spawnSync('tar', [ '-xf', - getNormalizedString(redisInsideArchivePath), + getNormalizedCIString(redisInsideArchivePath), '-C', - getNormalizedString(extractDir), + getNormalizedCIString(extractDir), '--strip-components', '1', 'api', diff --git a/src/webviews/src/actions/tests/processCliAction.spec.ts b/src/webviews/src/actions/tests/processCliAction.spec.ts new file mode 100644 index 00000000..1a3adcd2 --- /dev/null +++ b/src/webviews/src/actions/tests/processCliAction.spec.ts @@ -0,0 +1,16 @@ +import * as useCliSettingsThunks from 'uiSrc/modules/cli/hooks/cli-settings/useCliSettingsThunks' +import { constants } from 'testSrc/helpers' +import { processCliAction } from 'uiSrc/actions' + +vi.spyOn(useCliSettingsThunks, 'addCli') + +beforeEach(() => { + vi.stubGlobal('ri', { }) +}) + +describe('processCliAction', () => { + it('should call addCli', () => { + processCliAction(constants.VSCODE_CLI_ACTION) + expect(useCliSettingsThunks.addCli).toBeCalled() + }) +}) diff --git a/src/webviews/src/actions/tests/selectKeyAction.spec.ts b/src/webviews/src/actions/tests/selectKeyAction.spec.ts new file mode 100644 index 00000000..28779db0 --- /dev/null +++ b/src/webviews/src/actions/tests/selectKeyAction.spec.ts @@ -0,0 +1,23 @@ +import * as useSelectedKey from 'uiSrc/store/hooks/use-selected-key-store/useSelectedKeyStore' +import { selectKeyAction } from 'uiSrc/actions' +import { constants } from 'testSrc/helpers' + + +vi.spyOn(useSelectedKey, 'fetchKeyInfo') + + +beforeEach(() => { + vi.stubGlobal('ri', { }) + + useSelectedKey.useSelectedKeyStore.setState((state) => ({ + ...state, + data: constants.KEY_INFO, + })) +}) + +describe('selectKeyAction', () => { + it('should call fetchKeyInfo', () => { + selectKeyAction(constants.VSCODE_SELECT_KEY_ACTION) + expect(useSelectedKey.fetchKeyInfo).toBeCalled() + }) +}) diff --git a/src/webviews/src/components/auto-refresh/AutoRefresh.tsx b/src/webviews/src/components/auto-refresh/AutoRefresh.tsx index fc4ede88..89e1f89b 100644 --- a/src/webviews/src/components/auto-refresh/AutoRefresh.tsx +++ b/src/webviews/src/components/auto-refresh/AutoRefresh.tsx @@ -199,6 +199,8 @@ const AutoRefresh = React.memo(({ )} - {(loading || refreshing) &&
} + {(loading || refreshing) &&
}
) } diff --git a/src/webviews/src/modules/key-details/components/hash-details/HashDetails.tsx b/src/webviews/src/modules/key-details/components/hash-details/HashDetails.tsx index 567f438f..d16f8e71 100644 --- a/src/webviews/src/modules/key-details/components/hash-details/HashDetails.tsx +++ b/src/webviews/src/modules/key-details/components/hash-details/HashDetails.tsx @@ -84,7 +84,7 @@ const HashDetails = (props: Props) => { ), children, , - ]), []) + ]), [showTtl, isExpireFieldsAvailable]) return (
diff --git a/src/webviews/src/modules/key-details/components/string-details/StringDetails.tsx b/src/webviews/src/modules/key-details/components/string-details/StringDetails.tsx index ba7280d8..e5d7b4df 100644 --- a/src/webviews/src/modules/key-details/components/string-details/StringDetails.tsx +++ b/src/webviews/src/modules/key-details/components/string-details/StringDetails.tsx @@ -103,7 +103,7 @@ const StringDetails = (props: Props) => { setEditItem(!editItem) }} />, - ]), []) + ]), [isStringEditable, isEditable]) return (
diff --git a/src/webviews/src/modules/keys-tree/components/database-wrapper/DatabaseWrapper.spec.tsx b/src/webviews/src/modules/keys-tree/components/database-wrapper/DatabaseWrapper.spec.tsx index 0920838c..6e7aca91 100644 --- a/src/webviews/src/modules/keys-tree/components/database-wrapper/DatabaseWrapper.spec.tsx +++ b/src/webviews/src/modules/keys-tree/components/database-wrapper/DatabaseWrapper.spec.tsx @@ -8,6 +8,7 @@ import { apiService, vscodeApi } from 'uiSrc/services' import * as useContext from 'uiSrc/store/hooks/use-context/useContext' import * as useSelectedKeyStore from 'uiSrc/store/hooks/use-selected-key-store/useSelectedKeyStore' import { Database } from 'uiSrc/store' +import * as useDatabasesStore from 'uiSrc/store' import { constants, fireEvent, render, waitForStack } from 'testSrc/helpers' import { DatabaseWrapper, Props } from './DatabaseWrapper' import * as useKeys from '../../hooks/useKeys' @@ -39,12 +40,23 @@ const resetKeysTreeMock = vi.fn(); resetKeysTree: resetKeysTreeMock, })) +vi.spyOn(useDatabasesStore, 'fetchDatabaseOverviewById') + describe('DatabaseWrapper', () => { it('should render', () => { expect(render()).toBeTruthy() }) - it('should call fetchPatternKeysAction action after click on refresh icon', async () => { + it('should call fetchDatabaseOverviewById action after click on refresh icon', async () => { + const { queryByTestId } = render() + + fireEvent.click(queryByTestId('refresh-databases')!) + await waitForStack() + + expect(useDatabasesStore.fetchDatabaseOverviewById).toBeCalled() + }) + + it('should call fetchPatternKeysAction action after click on logical database refresh icon', async () => { const { queryByTestId } = render() fireEvent.click(queryByTestId(`database-${mockDatabase.id}`)!) diff --git a/src/webviews/src/modules/keys-tree/components/database-wrapper/DatabaseWrapper.tsx b/src/webviews/src/modules/keys-tree/components/database-wrapper/DatabaseWrapper.tsx index 1b14a9ac..c0f792ac 100644 --- a/src/webviews/src/modules/keys-tree/components/database-wrapper/DatabaseWrapper.tsx +++ b/src/webviews/src/modules/keys-tree/components/database-wrapper/DatabaseWrapper.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react' import cx from 'classnames' -import { VscEdit } from 'react-icons/vsc' -import { isUndefined, toNumber } from 'lodash' +import { VscEdit, VscRefresh } from 'react-icons/vsc' +import { isUndefined, toNumber, isEqual } from 'lodash' import * as l10n from '@vscode/l10n' import { VSCodeButton } from '@vscode/webview-ui-toolkit/react' @@ -11,7 +11,7 @@ import { getRedisModulesSummary, sendEventTelemetry, } from 'uiSrc/utils' -import { ContextStoreProvider, Database, DatabaseOverview, checkConnectToDatabase, deleteDatabases } from 'uiSrc/store' +import { ContextStoreProvider, Database, DatabaseOverview, checkConnectToDatabase, deleteDatabases, fetchDatabaseOverviewById } from 'uiSrc/store' import { Chevron, DatabaseIcon, Tooltip } from 'uiSrc/ui' import { PopoverDelete } from 'uiSrc/components' import { POPOVER_WINDOW_BORDER_WIDTH, StorageItem, VscodeMessageAction } from 'uiSrc/constants' @@ -28,9 +28,29 @@ export interface Props { database: Database } +const LogicalDatabase = ( + { database, open, dbTotal }: + { database: Database, open?: boolean, dbTotal?: number }, +) => ( + + + + + + + + + +) + export const DatabaseWrapper = React.memo(({ database }: Props) => { const { id, name } = database + const [loading, setLoading] = useState(false) const [showTree, setShowTree] = useState(false) const [totalKeysPerDb, setTotalKeysPerDb] = useState>>(undefined) @@ -82,6 +102,16 @@ export const DatabaseWrapper = React.memo(({ database }: Props) => { vscodeApi.postMessage({ action: VscodeMessageAction.EditDatabase, data: { database } }) } + const refreshHandle = async () => { + setLoading(true) + const overview = await fetchDatabaseOverviewById(id) + setLoading(false) + + if (!isEqual(totalKeysPerDb, overview?.totalKeysPerDb)) { + setTotalKeysPerDb(overview?.totalKeysPerDb) + } + } + const deleteDatabaseHandle = () => { deleteDatabases([database]) } @@ -95,25 +125,6 @@ export const DatabaseWrapper = React.memo(({ database }: Props) => { }) } - const LogicalDatabase = ( - { database, open, dbTotal }: - { database: Database, open?: boolean, dbTotal?: number }, - ) => ( - - - - - - - - - - ) - return (
@@ -138,6 +149,14 @@ export const DatabaseWrapper = React.memo(({ database }: Props) => {
+ + + { />
+ {loading &&
} {showTree && (<> {!isUndefined(totalKeysPerDb) && Object.keys(totalKeysPerDb).map((databaseIndex) => ( { )} */}
+ { {isSortingASC ? : } - diff --git a/src/webviews/src/modules/keys-tree/components/keys-tree-filter/KeyTreeFilter.spec.tsx b/src/webviews/src/modules/keys-tree/components/keys-tree-filter/KeyTreeFilter.spec.tsx index 0085dc1b..487a6536 100644 --- a/src/webviews/src/modules/keys-tree/components/keys-tree-filter/KeyTreeFilter.spec.tsx +++ b/src/webviews/src/modules/keys-tree/components/keys-tree-filter/KeyTreeFilter.spec.tsx @@ -3,7 +3,8 @@ import { Mock } from 'vitest' import { TelemetryEvent, sendEventTelemetry } from 'uiSrc/utils' import * as utils from 'uiSrc/utils' import * as moduleUtils from 'uiSrc/modules/keys-tree/utils' -import { apiService } from 'uiSrc/services' +import { apiService, sessionStorageService } from 'uiSrc/services' +import { StorageItem } from 'uiSrc/constants' import { constants, fireEvent, @@ -22,6 +23,8 @@ const TREE_FILTER_TRIGGER_BTN = 'key-tree-filter-trigger' const FILTER_SELECT = 'tree-view-filter-select' const SEARCH_INPUT = 'tree-view-search-input' +vi.spyOn(sessionStorageService, 'set') +vi.spyOn(sessionStorageService, 'get') vi.spyOn(utils, 'sendEventTelemetry') vi.spyOn(moduleUtils, 'parseKeysListResponse').mockImplementation(() => constants.KEYS_LIST) @@ -107,4 +110,62 @@ describe('KeyTreeDelimiter', () => { expect(setFilterAndSearchMock).toBeCalled() }) + + it('"setFilterAndSearch" should be called with stored values from sessionStorage', async () => { + const useKeysInContextMock = vi.spyOn(useKeys, 'useKeysInContext') + const setFilterAndSearchMock = vi.fn() + const mockSearch = 'search' + const mockFilter = 'hash'; + + (sessionStorageService.get as Mock).mockImplementation(() => ({ + search: mockSearch, + filter: mockFilter, + })) + + useKeysInContextMock.mockImplementation(() => ({ + dbId: constants.DATABASE_ID, + dbIndex: 1, + filter: ALL_KEY_TYPES_VALUE, + searchInit: '', + setFilterAndSearch: setFilterAndSearchMock, + })) + + render() + + expect(sessionStorageService.get).toBeCalledWith(`${StorageItem.keysTreeFilter + constants.DATABASE_ID + 1}`) + expect(setFilterAndSearchMock).toBeCalledWith(mockFilter, mockSearch) + }) + + it('should set values to sessionStorage on Apply', async () => { + const useKeysInContextMock = vi.spyOn(useKeys, 'useKeysInContext') + const setFilterAndSearchMock = vi.fn() + + const mockSearch = 'search' + const mockFilter = 'Hash' + + useKeysInContextMock.mockImplementation(() => ({ + dbId: constants.DATABASE_ID, + dbIndex: 1, + setFilterAndSearch: setFilterAndSearchMock, + })) + + render() + + fireEvent.click(screen.getByTestId(TREE_FILTER_TRIGGER_BTN)) + + fireEvent.input(screen.getByTestId(SEARCH_INPUT), { target: { value: mockSearch } }) + + fireEvent.click(screen.getByTestId(FILTER_SELECT)) + + fireEvent.click(await screen.findByText(mockFilter)) + + await waitForStack() + + fireEvent.click(screen.getByTestId(APPLY_BTN)) + + expect(sessionStorageService.set).toBeCalledWith( + `${StorageItem.keysTreeFilter + constants.DATABASE_ID + 1}`, + { search: mockSearch, filter: mockFilter.toLowerCase() }) + expect(setFilterAndSearchMock).toBeCalledWith(mockFilter.toLowerCase(), mockSearch) + }) }) diff --git a/src/webviews/src/modules/keys-tree/components/keys-tree-filter/KeyTreeFilter.tsx b/src/webviews/src/modules/keys-tree/components/keys-tree-filter/KeyTreeFilter.tsx index 1d3d7925..be5dc262 100644 --- a/src/webviews/src/modules/keys-tree/components/keys-tree-filter/KeyTreeFilter.tsx +++ b/src/webviews/src/modules/keys-tree/components/keys-tree-filter/KeyTreeFilter.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import cx from 'classnames' import * as l10n from '@vscode/l10n' import Popup from 'reactjs-popup' @@ -8,7 +8,8 @@ import { PopupActions } from 'reactjs-popup/dist/types' import { useShallow } from 'zustand/react/shallow' import { InputText, Select, SelectOption } from 'uiSrc/ui' -import { DEFAULT_SEARCH_MATCH, KeyTypes, Keys } from 'uiSrc/constants' +import { DEFAULT_SEARCH_MATCH, KeyTypes, Keys, StorageItem } from 'uiSrc/constants' +import { sessionStorageService } from 'uiSrc/services' import { ALL_KEY_TYPES_VALUE, FILTER_KEY_TYPE_OPTIONS } from './constants' import { useKeysApi, useKeysInContext } from '../../hooks/useKeys' import styles from './styles.module.scss' @@ -19,9 +20,11 @@ export interface Props { const sortOptions: SelectOption[] = FILTER_KEY_TYPE_OPTIONS export const KeyTreeFilter = () => { - const { filter, isSearched, isFiltered, searchInit, setFilterAndSearch } = useKeysInContext(useShallow((state) => ({ + const { filter, isSearched, isFiltered, searchInit, dbId, dbIndex, setFilterAndSearch } = useKeysInContext(useShallow((state) => ({ isSearched: state.isSearched, isFiltered: state.isFiltered, + dbId: state.databaseId, + dbIndex: state.databaseIndex, filter: state.filter || ALL_KEY_TYPES_VALUE, searchInit: state.search === DEFAULT_SEARCH_MATCH ? '' : state.search, setFilterAndSearch: state.setFilterAndSearch, @@ -33,6 +36,17 @@ export const KeyTreeFilter = () => { const { fetchPatternKeysAction } = useKeysApi() + useEffect(() => { + const settings = sessionStorageService.get(`${StorageItem.keysTreeFilter + dbId + dbIndex}`) + + if (settings) { + setSearch(settings.search) + onChangeType(settings.filter) + + handleApply(settings.filter, settings.search) + } + }, []) + const closePopover = () => { popupRef.current?.close?.() } @@ -43,10 +57,15 @@ export const KeyTreeFilter = () => { setSearch(searchInit) } - const handleApply = () => { - const filter = typeSelected === ALL_KEY_TYPES_VALUE ? null : typeSelected + const handleApply = (filterInit: string = typeSelected, searchInit: string = search) => { + const filter = filterInit === ALL_KEY_TYPES_VALUE ? null : filterInit + + sessionStorageService.set( + `${StorageItem.keysTreeFilter + dbId + dbIndex}`, + { filter, search: searchInit }, + ) - setFilterAndSearch(filter as KeyTypes, search) + setFilterAndSearch(filter as KeyTypes, searchInit) fetchPatternKeysAction() closePopover() } @@ -59,6 +78,11 @@ export const KeyTreeFilter = () => { setTypeSelected(ALL_KEY_TYPES_VALUE) setSearch('') + sessionStorageService.set( + `${StorageItem.keysTreeFilter + dbId + dbIndex}`, + undefined, + ) + setFilterAndSearch(null, '') fetchPatternKeysAction() closePopover() @@ -78,7 +102,7 @@ export const KeyTreeFilter = () => { repositionOnResize keepTooltipInside={false} className="key-tree-filter-popup" - position="bottom right" + position="bottom center" trigger={() => ( { handleApply()} > {l10n.t('Save')} diff --git a/src/webviews/src/modules/keys-tree/components/keys-tree-header/KeysTreeHeader.tsx b/src/webviews/src/modules/keys-tree/components/keys-tree-header/KeysTreeHeader.tsx index 39ed6fb0..4bd807e1 100644 --- a/src/webviews/src/modules/keys-tree/components/keys-tree-header/KeysTreeHeader.tsx +++ b/src/webviews/src/modules/keys-tree/components/keys-tree-header/KeysTreeHeader.tsx @@ -35,6 +35,10 @@ export const KeysTreeHeader = ({ database, open, dbTotal, children }: Props) => keysApi.fetchMorePatternKeysAction(nextCursor, SCAN_TREE_COUNT_DEFAULT) } + useEffect(() => { + keysApi.fetchPatternKeysAction() + }, [dbTotal, total]) + useEffect(() => { if (showTree) { sendEventTelemetry({ @@ -66,7 +70,7 @@ export const KeysTreeHeader = ({ database, open, dbTotal, children }: Props) => database={database} loading={loading} scanned={scanned} - total={dbTotal ?? total} + total={showTree ? total : (dbTotal ?? null)} dbIndex={dbIndex} resultsLength={resultsLength} showTree={showTree} diff --git a/src/webviews/src/modules/keys-tree/components/virtual-tree/VirtualTree.tsx b/src/webviews/src/modules/keys-tree/components/virtual-tree/VirtualTree.tsx index 9f0229a8..da9c2a94 100644 --- a/src/webviews/src/modules/keys-tree/components/virtual-tree/VirtualTree.tsx +++ b/src/webviews/src/modules/keys-tree/components/virtual-tree/VirtualTree.tsx @@ -269,7 +269,7 @@ const VirtualTree = (props: Props) => { itemSize={22} treeWalker={treeWalker} onItemsRendered={onItemsRendered} - className={cx(styles.customScroll, { 'table-loading': loading })} + className={cx(styles.customScroll, { 'data-loading': loading })} > {Node} diff --git a/src/webviews/src/modules/keys-tree/hooks/useKeysThunks.ts b/src/webviews/src/modules/keys-tree/hooks/useKeysThunks.ts index b63b777d..3a47102d 100644 --- a/src/webviews/src/modules/keys-tree/hooks/useKeysThunks.ts +++ b/src/webviews/src/modules/keys-tree/hooks/useKeysThunks.ts @@ -261,7 +261,7 @@ KeysThunks eventData: { keyType, TTL: data.expire || -1, - databaseId: sessionStorageService.get(StorageItem.databaseId), + databaseId: window.ri?.database?.id ?? null, length: getLengthByKeyType(keyType, data), }, }) diff --git a/src/webviews/src/store/hooks/use-databases-store/useDatabasesStore.ts b/src/webviews/src/store/hooks/use-databases-store/useDatabasesStore.ts index 1aa37877..3aa014b4 100644 --- a/src/webviews/src/store/hooks/use-databases-store/useDatabasesStore.ts +++ b/src/webviews/src/store/hooks/use-databases-store/useDatabasesStore.ts @@ -178,6 +178,7 @@ export const fetchDatabaseById = async (databaseId: string, onSuccess?: (data: D export const fetchDatabaseOverviewById = async (databaseId: string): Promise> => { try { + useDatabasesStore.getState().processDatabase() const { data, status } = await apiService.get( `${ApiEndpoints.DATABASES}/${databaseId}/overview`, { params: { keyspace: 'full' }, @@ -188,6 +189,8 @@ export const fetchDatabaseOverviewById = async (databaseId: string): Promise