Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export class KeysController extends BaseController {
);
}

@Post('get-infos')
@Post('get-metadata')
@HttpCode(200)
@ApiOperation({ description: 'Get info for multiple keys' })
@ApiBody({ type: GetKeysInfoDto })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const { server, request, constants, rte } = deps;

// endpoint to test
const endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>
request(server).post(`/instance/${instanceId}/keys/get-infos`);
request(server).post(`/instance/${instanceId}/keys/get-metadata`);

const responseSchema = Joi.array().items(Joi.object().keys({
name: JoiRedisString.required(),
Expand All @@ -37,7 +37,7 @@ const mainCheckFn = async (testCase) => {
});
};

describe('POST /instance/:instanceId/keys/get-infos', () => {
describe('POST /instance/:instanceId/keys/get-metadata', () => {
before(async () => await rte.data.generateKeys(true));

describe('Modes', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const Node = ({
}, [keys, isSelected])

const handleClick = () => {
if (isLeaf && keys) {
if (isLeaf && keys && !isSelected) {
setItems?.(keys)
updateStatusSelected?.(fullName, keys)
}
Expand Down
1 change: 1 addition & 0 deletions redisinsight/ui/src/constants/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ enum ApiEndpoints {
REDIS_CLOUD_DATABASES = 'redis-enterprise/cloud/get-databases',
SENTINEL_MASTERS = 'sentinel/get-masters',
KEYS = 'keys',
KEYS_METADATA = 'keys/get-metadata',
KEY_INFO = 'keys/get-info',
KEY_NAME = 'keys/name',
KEY_TTL = 'keys/ttl',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react'
import { render } from 'uiSrc/utils/test-utils'
import { render, waitFor } from 'uiSrc/utils/test-utils'
import { KeysStoreData, KeyViewType } from 'uiSrc/slices/interfaces/keys'
import { keysSelector, setLastBatchKeys } from 'uiSrc/slices/browser/keys'
import { apiService } from 'uiSrc/services'
import KeyList from './KeyList'

const propsMock = {
Expand Down Expand Up @@ -98,4 +99,75 @@ describe('KeyList', () => {

expect(setLastBatchKeys).not.toBeCalled()
})

it('should call apiService.post to get key info', async () => {
const apiServiceMock = jest.fn().mockResolvedValue([...propsMock.keysState.keys])
apiService.post = apiServiceMock

const { rerender } = render(<KeyList {...propsMock} keysState={{ ...propsMock.keysState, keys: [] }} />)

rerender(<KeyList
{...propsMock}
keysState={{
...propsMock.keysState,
keys: propsMock.keysState.keys.map(({ name }) => ({ name })) }}
/>)

await waitFor(async () => {
expect(apiServiceMock).toBeCalled()
}, { timeout: 150 })
})

it('apiService.post should be called with only keys without info', async () => {
const params = { params: { encoding: 'buffer' } }
const apiServiceMock = jest.fn().mockResolvedValue([...propsMock.keysState.keys])
apiService.post = apiServiceMock

const { rerender } = render(<KeyList {...propsMock} keysState={{ ...propsMock.keysState, keys: [] }} />)

rerender(<KeyList
{...propsMock}
keysState={{
...propsMock.keysState,
keys: [
...propsMock.keysState.keys.map(({ name }) => ({ name })),
{ name: 'key5', size: 100, length: 100 }, // key with info
] }}
/>)

await waitFor(async () => {
expect(apiServiceMock).toBeCalledTimes(2)

expect(apiServiceMock.mock.calls[0]).toEqual([
'/instance//keys/get-metadata',
{ keys: ['key1'] },
params,
])

expect(apiServiceMock.mock.calls[1]).toEqual([
'/instance//keys/get-metadata',
{ keys: ['key1', 'key2', 'key3'] },
params,
])
}, { timeout: 150 })
})

it('key info loadings (type, ttl, size) should be in the DOM if keys do not have info', async () => {
const { rerender, queryAllByTestId } = render(
<KeyList {...propsMock} keysState={{ ...propsMock.keysState, keys: [] }} />
)

rerender(<KeyList
{...propsMock}
keysState={{
...propsMock.keysState,
keys: [
...propsMock.keysState.keys.map(({ name }) => ({ name })),
] }}
/>)

expect(queryAllByTestId(/ttl-loading/).length).toEqual(propsMock.keysState.keys.length)
expect(queryAllByTestId(/type-loading/).length).toEqual(propsMock.keysState.keys.length)
expect(queryAllByTestId(/size-loading/).length).toEqual(propsMock.keysState.keys.length)
})
})
123 changes: 98 additions & 25 deletions redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef,
import { useDispatch, useSelector } from 'react-redux'
import cx from 'classnames'
import { useParams } from 'react-router-dom'
import { debounce, isUndefined, reject } from 'lodash'

import {
EuiText,
EuiToolTip,
EuiTextColor,
EuiLoadingContent,
} from '@elastic/eui'
import {
formatBytes,
Expand All @@ -25,6 +27,7 @@ import {
ScanNoResultsFoundText,
} from 'uiSrc/constants/texts'
import {
fetchKeysMetadata,
keysDataSelector,
keysSelector,
selectedKeySelector,
Expand All @@ -40,7 +43,7 @@ import { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api'
import { KeysStoreData, KeyViewType } from 'uiSrc/slices/interfaces/keys'
import VirtualTable from 'uiSrc/components/virtual-table/VirtualTable'
import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces'
import { OVER_RENDER_BUFFER_COUNT, Pages, TableCellAlignment, TableCellTextAlignment } from 'uiSrc/constants'
import { Pages, TableCellAlignment, TableCellTextAlignment } from 'uiSrc/constants'
import { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys'
import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'

Expand Down Expand Up @@ -70,9 +73,10 @@ const KeyList = forwardRef((props: Props, ref) => {
const { isSearched, isFiltered, viewType } = useSelector(keysSelector)
const { keyList: { scrollTopPosition } } = useSelector(appContextBrowser)

const [items, setItems] = useState(keysState.keys)
const [, rerender] = useState({})

const formattedLastIndexRef = useRef(OVER_RENDER_BUFFER_COUNT)
const itemsRef = useRef(keysState.keys)
const renderedRowsIndexesRef = useRef({ startIndex: 0, lastIndex: 0 })

const dispatch = useDispatch()

Expand All @@ -87,20 +91,20 @@ const KeyList = forwardRef((props: Props, ref) => {
if (viewType === KeyViewType.Tree) {
return
}
setItems((prevItems) => {
dispatch(setLastBatchKeys(prevItems.slice(-SCAN_COUNT_DEFAULT)))
return []
rerender(() => {
dispatch(setLastBatchKeys(itemsRef.current?.slice(-SCAN_COUNT_DEFAULT)))
})
}, [])

useEffect(() => {
const newKeys = bufferFormatRangeItems(keysState.keys, 0, OVER_RENDER_BUFFER_COUNT, formatItem)

if (keysState.keys.length < items.length) {
formattedLastIndexRef.current = 0
itemsRef.current = [...keysState.keys]
if (itemsRef.current.length === 0) {
return
}

setItems(newKeys)
const { lastIndex, startIndex } = renderedRowsIndexesRef.current
onRowsRendered(startIndex, lastIndex)
rerender({})
}, [keysState.keys])

const onNoKeysLinkClick = () => {
Expand Down Expand Up @@ -130,8 +134,7 @@ const KeyList = forwardRef((props: Props, ref) => {
}

const onLoadMoreItems = (props: { startIndex: number, stopIndex: number }) => {
const formattedAllKeys = bufferFormatRangeItems(items, formattedLastIndexRef.current, items.length, formatItem)
loadMoreItems?.(formattedAllKeys, props)
loadMoreItems?.(itemsRef.current, props)
}

const onWheelSearched = (event: React.WheelEvent) => {
Expand Down Expand Up @@ -159,34 +162,96 @@ const KeyList = forwardRef((props: Props, ref) => {
nameString: bufferToString(item.name)
}), [])

const bufferFormatRows = (lastIndex: number) => {
const newItems = bufferFormatRangeItems(items, formattedLastIndexRef.current, lastIndex, formatItem)
const onRowsRendered = debounce(async (startIndex: number, lastIndex: number) => {
renderedRowsIndexesRef.current = { lastIndex, startIndex }

setItems(newItems)
const newItems = bufferFormatRows(startIndex, lastIndex)

if (lastIndex > formattedLastIndexRef.current) {
formattedLastIndexRef.current = lastIndex
}
getMetadata(startIndex, lastIndex, newItems)
}, 100)

const bufferFormatRows = (startIndex: number, lastIndex: number): GetKeyInfoResponse[] => {
const newItems = bufferFormatRangeItems(
itemsRef.current, startIndex, lastIndex, formatItem
)
itemsRef.current.splice(startIndex, newItems.length, ...newItems)

return newItems
}

const getMetadata = (
startIndex: number,
lastIndex: number,
itemsInit: GetKeyInfoResponse[] = []
): void => {
const isSomeNotUndefined = ({ type, size, length }: GetKeyInfoResponse) =>
!isUndefined(type) || !isUndefined(size) || !isUndefined(length)

const emptyItems = reject(itemsInit, isSomeNotUndefined)

if (!emptyItems.length) return

dispatch(fetchKeysMetadata(
emptyItems.map(({ name }) => name),
(loadedItems) =>
onSuccessFetchedMetadata({
startIndex,
lastIndex,
loadedItems,
isFirstEmpty: !isSomeNotUndefined(itemsInit[0]),
})
))
}

const onSuccessFetchedMetadata = (data: {
startIndex: number,
lastIndex: number,
isFirstEmpty: boolean
loadedItems: GetKeyInfoResponse[],
}) => {
const {
startIndex,
lastIndex,
isFirstEmpty,
loadedItems,
} = data
const items = loadedItems.map(formatItem)
const startIndexDel = isFirstEmpty ? startIndex : lastIndex - items.length + 1

itemsRef.current.splice(startIndexDel, items.length, ...items)

rerender({})
}

const columns: ITableColumn[] = [
{
id: 'type',
label: 'Type',
absoluteWidth: 'auto',
minWidth: 126,
render: (cellData: any, { nameString: name }: any) => <GroupBadge type={cellData} name={name} />,
render: (cellData: any, { nameString: name }: any) => (
isUndefined(cellData)
? <EuiLoadingContent lines={1} className={styles.keyInfoLoading} data-testid="type-loading" />
: <GroupBadge type={cellData} name={name} />
)
},
{
id: 'nameString',
label: 'Key',
minWidth: 100,
truncateText: true,
render: (cellData: string = '') => {
render: (cellData: string) => {
if (isUndefined(cellData)) {
return (
<EuiLoadingContent
lines={1}
className={cx(styles.keyInfoLoading, styles.keyNameLoading)}
data-testid="name-loading"
/>
)
}
// Better to cut the long string, because it could affect virtual scroll performance
const name = cellData
const name = cellData || ''
const cellContent = replaceSpaces(name?.substring(0, 200))
const tooltipContent = formatLongName(name)
return (
Expand Down Expand Up @@ -214,6 +279,9 @@ const KeyList = forwardRef((props: Props, ref) => {
truncateText: true,
alignment: TableCellAlignment.Right,
render: (cellData: number, { nameString: name }: GetKeyInfoResponse) => {
if (isUndefined(cellData)) {
return <EuiLoadingContent lines={1} className={styles.keyInfoLoading} data-testid="ttl-loading" />
}
if (cellData === -1) {
return (
<EuiTextColor color="subdued" data-testid={`ttl-${name}`}>
Expand Down Expand Up @@ -252,6 +320,10 @@ const KeyList = forwardRef((props: Props, ref) => {
alignment: TableCellAlignment.Right,
textAlignment: TableCellTextAlignment.Right,
render: (cellData: number, { nameString: name }: GetKeyInfoResponse) => {
if (isUndefined(cellData)) {
return <EuiLoadingContent lines={1} className={styles.keyInfoLoading} data-testid="size-loading" />
}

if (!cellData) {
return (
<EuiText color="subdued" size="s" style={{ maxWidth: '100%' }} data-testid={`size-${name}`}>
Expand Down Expand Up @@ -297,15 +369,16 @@ const KeyList = forwardRef((props: Props, ref) => {
loadMoreItems={onLoadMoreItems}
onWheel={onWheelSearched}
loading={loading}
items={items}
items={itemsRef.current}
totalItemsCount={keysState.total ? keysState.total : Infinity}
scanned={isSearched || isFiltered ? keysState.scanned : 0}
noItemsMessage={getNoItemsMessage()}
selectedKey={selectedKey}
scrollTopProp={scrollTopPosition}
setScrollTopPosition={setScrollTopPosition}
hideFooter={hideFooter}
onRowsRendered={({ overscanStopIndex }) => bufferFormatRows(overscanStopIndex)}
onRowsRendered={({ overscanStartIndex, overscanStopIndex }) =>
onRowsRendered(overscanStartIndex, overscanStopIndex)}
/>
</div>
</div>
Expand All @@ -314,4 +387,4 @@ const KeyList = forwardRef((props: Props, ref) => {
)
})

export default KeyList
export default React.memo(KeyList)
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@
transition: opacity 250ms ease-in-out;
}

.keyInfoLoading {
width: 44px;

:global(.euiLoadingContent__singleLine) {
margin-bottom: 0;
}
}

.keyNameLoading {
width: 50%;
min-width: 100px;
max-width: 300px;
}

:global(.table-row-selected) .action {
opacity: 1;
}
Loading