Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ module.exports = {
],
coverageThreshold: {
global: {
statements: 77,
branches: 55,
functions: 65,
lines: 75,
statements: 78,
branches: 61,
functions: 70,
lines: 79,
},
// './redisinsight/ui/src/slices/**/*.ts': {
// statements: 90,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@
"electron-log": "^4.2.4",
"electron-store": "^8.0.0",
"electron-updater": "^5.0.5",
"fflate": "^0.7.4",
"file-saver": "^2.0.5",
"formik": "^2.2.9",
"html-entities": "^2.3.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { cloneDeep } from 'lodash'
import React from 'react'
import { instance, mock } from 'ts-mockito'
import { cleanup, mockedStore, render, fireEvent, act, screen, waitForEuiToolTipVisible } from 'uiSrc/utils/test-utils'
import QueryCardHeader, { Props } from './QueryCardHeader'
import { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'
import { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers'
import QueryCardHeader, { Props } from './QueryCardHeader'

const mockedProps = mock<Props>()

Expand Down
6 changes: 6 additions & 0 deletions redisinsight/ui/src/constants/browser.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { KeyValueFormat } from "./keys"

export const DEFAULT_DELIMITER = ':'

export const TEXT_UNPRINTABLE_CHARACTERS = {
Expand All @@ -10,3 +12,7 @@ export const TEXT_INVALID_VALUE = {
title: 'Value will be saved as Unicode',
text: 'as it is not valid in the selected format.',
}

export const TEXT_DISABLED_COMPRESSED_VALUE: string = 'Cannot edit the decompressed value'

export const TEXT_FAILED_CONVENT_FORMATTER = (format: KeyValueFormat) => `Failed to convert to ${format}`
13 changes: 13 additions & 0 deletions redisinsight/ui/src/constants/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,19 @@ export enum KeyValueFormat {
Pickle = 'Pickle',
}

export enum KeyValueCompressor {
GZIP = 'GZIP',
LZ4 = 'LZ4',
ZSTD = 'ZSTD',
SNAPPY = 'SNAPPY',
Brotli = 'Brotli',
PHPGZCompress = 'PHPGZCompress',
}

export const COMPRESSOR_MAGIC_SYMBOLS = Object.freeze({
[KeyValueCompressor.GZIP]: [31, 139], // 1f 8b hex
})

export enum SearchHistoryMode {
Pattern = 'pattern',
Redisearch = 'redisearch'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import React from 'react'
import { instance, mock } from 'ts-mockito'
import { TEXT_DISABLED_COMPRESSED_VALUE } from 'uiSrc/constants'
import { hashDataSelector } from 'uiSrc/slices/browser/hash'
import { RedisResponseBufferType } from 'uiSrc/slices/interfaces'
import { bufferToString } from 'uiSrc/utils'
import { fireEvent, render, screen } from 'uiSrc/utils/test-utils'
import { anyToBuffer, bufferToString } from 'uiSrc/utils'
import { act, fireEvent, render, screen, waitForEuiToolTipVisible } from 'uiSrc/utils/test-utils'
import { GZIP_COMPRESSED_VALUE_1, GZIP_COMPRESSED_VALUE_2, GZIP_DECOMPRESSED_VALUE_1, GZIP_DECOMPRESSED_VALUE_2 } from 'uiSrc/utils/tests/decompressors/decompressors.spec'
import HashDetails, { Props } from './HashDetails'

const mockedProps = mock<Props>()
Expand Down Expand Up @@ -70,4 +73,52 @@ describe('HashDetails', () => {
render(<HashDetails {...instance(mockedProps)} />)
expect(screen.getByTestId('resize-trigger-field')).toBeInTheDocument()
})

describe('decompressed data', () => {
it('should render decompressed GZIP data', () => {
const defaultState = jest.requireActual('uiSrc/slices/browser/hash').initialState
const hashDataSelectorMock = jest.fn().mockReturnValue({
...defaultState,
total: 1,
key: '123zxczxczxc',
fields: [
{ field: anyToBuffer(GZIP_COMPRESSED_VALUE_1), value: anyToBuffer(GZIP_COMPRESSED_VALUE_2) },
]
})
hashDataSelector.mockImplementation(hashDataSelectorMock)

const { queryByTestId, queryAllByTestId } = render(<HashDetails {...instance(mockedProps)} />)
const fieldEl = queryAllByTestId(/hash-field-/)?.[0]
const valueEl = queryByTestId(/hash-field-value/)

expect(fieldEl).toHaveTextContent(GZIP_DECOMPRESSED_VALUE_1)
expect(valueEl).toHaveTextContent(GZIP_DECOMPRESSED_VALUE_2)
})

it('edit button should be disabled if data was compressed', async () => {
const defaultState = jest.requireActual('uiSrc/slices/browser/hash').initialState
const hashDataSelectorMock = jest.fn().mockReturnValue({
...defaultState,
total: 1,
key: '123zxczxczxc',
fields: [
{ field: anyToBuffer(GZIP_COMPRESSED_VALUE_1), value: anyToBuffer(GZIP_COMPRESSED_VALUE_2) },
]
})
hashDataSelector.mockImplementation(hashDataSelectorMock)
const { queryByTestId } = render(<HashDetails {...instance(mockedProps)} />)
const editBtn = queryByTestId(/edit-hash-button/)

fireEvent.click(editBtn)

await act(async () => {
fireEvent.mouseOver(editBtn)
})
await waitForEuiToolTipVisible()

expect(editBtn).toBeDisabled()
expect(screen.getByTestId('hash-edit-tooltip')).toHaveTextContent(TEXT_DISABLED_COMPRESSED_VALUE)
expect(queryByTestId('hash-value-editor')).not.toBeInTheDocument()
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import {
KeyTypes,
OVER_RENDER_BUFFER_COUNT,
TableCellAlignment,
TEXT_DISABLED_COMPRESSED_VALUE,
TEXT_DISABLED_FORMATTER_EDITING,
TEXT_FAILED_CONVENT_FORMATTER,
TEXT_INVALID_VALUE,
TEXT_UNPRINTABLE_CHARACTERS
} from 'uiSrc/constants'
Expand Down Expand Up @@ -53,6 +55,7 @@ import {
stringToSerializedBufferFormat
} from 'uiSrc/utils'
import { stringToBuffer } from 'uiSrc/utils/formatters/bufferFormatters'
import { decompressingBuffer, getCompressor } from 'uiSrc/utils/decompressors'
import { AddFieldsToHashDto, GetHashFieldsResponse, HashFieldDto, } from 'apiSrc/modules/browser/dto/hash.dto'

import PopoverDelete from '../popover-delete/PopoverDelete'
Expand Down Expand Up @@ -292,17 +295,18 @@ const HashDetails = (props: Props) => {
className: 'value-table-separate-border',
headerClassName: 'value-table-separate-border',
render: (_name: string, { field: fieldItem }: HashFieldDto, expanded?: boolean) => {
// Better to cut the long string, because it could affect virtual scroll performance
const { value: decompressedItem } = decompressingBuffer(fieldItem)
const field = bufferToString(fieldItem) || ''
// Better to cut the long string, because it could affect virtual scroll performance
const tooltipContent = formatLongName(field)
const { value, isValid } = formattingBuffer(fieldItem, viewFormatProp, { expanded })
const { value, isValid } = formattingBuffer(decompressedItem, viewFormatProp, { expanded })

return (
<EuiText color="subdued" size="s" style={{ maxWidth: '100%', whiteSpace: 'break-spaces' }}>
<div style={{ display: 'flex' }} data-testid={`hash-field-${field}`}>
{!expanded && (
<EuiToolTip
title={isValid ? 'Field' : `Failed to convert to ${viewFormatProp}`}
title={isValid ? 'Field' : TEXT_FAILED_CONVENT_FORMATTER(viewFormatProp)}
className={styles.tooltip}
anchorClassName="truncateText"
position="bottom"
Expand All @@ -329,11 +333,13 @@ const HashDetails = (props: Props) => {
expanded?: boolean,
rowIndex = 0
) {
// Better to cut the long string, because it could affect virtual scroll performance
const { value: decompressedFieldItem } = decompressingBuffer(fieldItem)
const { value: decompressedValueItem } = decompressingBuffer(valueItem)
const value = bufferToString(valueItem)
const field = bufferToString(fieldItem)
const field = bufferToString(decompressedFieldItem)
// Better to cut the long string, because it could affect virtual scroll performance
const tooltipContent = formatLongName(value)
const { value: formattedValue, isValid } = formattingBuffer(valueItem, viewFormatProp, { expanded })
const { value: formattedValue, isValid } = formattingBuffer(decompressedValueItem, viewFormatProp, { expanded })

if (rowIndex === editingIndex) {
const disabled = !isNonUnicodeFormatter(viewFormat, isValid)
Expand Down Expand Up @@ -404,7 +410,7 @@ const HashDetails = (props: Props) => {
>
{!expanded && (
<EuiToolTip
title={isValid ? 'Value' : `Failed to convert to ${viewFormatProp}`}
title={isValid ? 'Value' : TEXT_FAILED_CONVENT_FORMATTER(viewFormatProp)}
className={styles.tooltip}
position="bottom"
content={tooltipContent}
Expand All @@ -428,12 +434,14 @@ const HashDetails = (props: Props) => {
minWidth: 95,
maxWidth: 95,
render: function Actions(_act: any, { field: fieldItem, value: valueItem }: HashFieldDto, _, rowIndex?: number) {
const compressor = getCompressor(valueItem)
const field = bufferToString(fieldItem, viewFormat)
const isEditable = isFormatEditable(viewFormat)
const isEditable = !compressor && isFormatEditable(viewFormat)
const tooltipContent = compressor ? TEXT_DISABLED_COMPRESSED_VALUE : TEXT_DISABLED_FORMATTER_EDITING
return (
<StopPropagation>
<div className="value-table-actions">
<EuiToolTip content={!isEditable ? TEXT_DISABLED_FORMATTER_EDITING : null}>
<EuiToolTip content={!isEditable ? tooltipContent : null} data-testid="hash-edit-tooltip">
<EuiButtonIcon
iconType="pencil"
aria-label="Edit field"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import {
ModulesKeyTypes,
STREAM_ADD_ACTION,
TEXT_DISABLED_FORMATTER_EDITING,
TEXT_UNPRINTABLE_CHARACTERS
TEXT_UNPRINTABLE_CHARACTERS,
TEXT_DISABLED_COMPRESSED_VALUE,
} from 'uiSrc/constants'
import { AddCommonFieldsFormConfig } from 'uiSrc/pages/browser/components/add-key/constants/fields-config'
import { initialKeyInfo, keysSelector, selectedKeyDataSelector, selectedKeySelector } from 'uiSrc/slices/browser/keys'
Expand Down Expand Up @@ -96,7 +97,7 @@ const KeyDetailsHeader = ({
const { id: instanceId } = useSelector(connectedInstanceSelector)
const { viewType } = useSelector(keysSelector)
const { viewType: streamViewType } = useSelector(streamSelector)
const { viewFormat: viewFormatProp } = useSelector(selectedKeySelector)
const { viewFormat: viewFormatProp, compressor } = useSelector(selectedKeySelector)

const [isPopoverDeleteOpen, setIsPopoverDeleteOpen] = useState(false)

Expand Down Expand Up @@ -320,7 +321,8 @@ const KeyDetailsHeader = ({
)

const Actions = (width: number) => {
const isEditable = isFormatEditable(viewFormatProp)
const isEditable = !compressor && isFormatEditable(viewFormatProp)
const noEditableText = compressor ? TEXT_DISABLED_COMPRESSED_VALUE : TEXT_DISABLED_FORMATTER_EDITING
return (
<>
{KEY_TYPES_ACTIONS[keyType] && 'addItems' in KEY_TYPES_ACTIONS[keyType] && (
Expand Down Expand Up @@ -401,7 +403,8 @@ const KeyDetailsHeader = ({
{KEY_TYPES_ACTIONS[keyType] && 'editItem' in KEY_TYPES_ACTIONS[keyType] && (
<div className={styles.actionBtn}>
<EuiToolTip
content={!isEditable ? TEXT_DISABLED_FORMATTER_EDITING : null}
content={!isEditable ? noEditableText : null}
data-testid="edit-key-value-tooltip"
>
<EuiButtonIcon
disabled={!isEditable}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import React from 'react'
import { mock } from 'ts-mockito'
import { act, fireEvent, render, screen } from 'uiSrc/utils/test-utils'
import { TEXT_DISABLED_COMPRESSED_VALUE } from 'uiSrc/constants'
import { listDataSelector } from 'uiSrc/slices/browser/list'
import { anyToBuffer } from 'uiSrc/utils'
import { act, fireEvent, render, screen, waitForEuiToolTipVisible } from 'uiSrc/utils/test-utils'
import { GZIP_COMPRESSED_VALUE_1, GZIP_DECOMPRESSED_VALUE_1 } from 'uiSrc/utils/tests/decompressors/decompressors.spec'
import ListDetails, { Props } from './ListDetails'

const mockedProps = mock<Props>()
Expand Down Expand Up @@ -68,4 +72,49 @@ describe('ListDetails', () => {
render(<ListDetails {...mockedProps} />)
expect(screen.getByTestId('resize-trigger-index')).toBeInTheDocument()
})

describe('decompressed data', () => {
it('should render decompressed GZIP data = "1"', () => {
const defaultState = jest.requireActual('uiSrc/slices/browser/list').initialState
const listDataSelectorMock = jest.fn().mockReturnValue({
...defaultState,
key: '123zxczxczxc',
elements: [
{ element: anyToBuffer(GZIP_COMPRESSED_VALUE_1), index: 0 },
]
})
listDataSelector.mockImplementation(listDataSelectorMock)

const { queryByTestId } = render(<ListDetails {...(mockedProps)} />)
const elementEl = queryByTestId(/list-element-value-/)

expect(elementEl).toHaveTextContent(GZIP_DECOMPRESSED_VALUE_1)
})

it('edit button should be disabled if data was compressed', async () => {
const defaultState = jest.requireActual('uiSrc/slices/browser/list').initialState
const listDataSelectorMock = jest.fn().mockReturnValue({
...defaultState,
key: '123zxczxczxc',
elements: [
{ element: anyToBuffer(GZIP_COMPRESSED_VALUE_1), index: 0 },
]
})
listDataSelector.mockImplementation(listDataSelectorMock)

const { queryByTestId } = render(<ListDetails {...(mockedProps)} />)
const editBtn = queryByTestId(/edit-list-button-/)

fireEvent.click(editBtn)

await act(async () => {
fireEvent.mouseOver(editBtn)
})
await waitForEuiToolTipVisible()

expect(editBtn).toBeDisabled()
expect(screen.getByTestId('list-edit-tooltip')).toHaveTextContent(TEXT_DISABLED_COMPRESSED_VALUE)
expect(queryByTestId('list-value-editor')).not.toBeInTheDocument()
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ import {
TableCellAlignment,
TEXT_INVALID_VALUE,
TEXT_DISABLED_FORMATTER_EDITING,
TEXT_UNPRINTABLE_CHARACTERS
TEXT_UNPRINTABLE_CHARACTERS,
TEXT_DISABLED_COMPRESSED_VALUE,
TEXT_FAILED_CONVENT_FORMATTER,
} from 'uiSrc/constants'
import {
bufferToSerializedFormat,
Expand All @@ -52,6 +54,8 @@ import VirtualTable from 'uiSrc/components/virtual-table/VirtualTable'
import InlineItemEditor from 'uiSrc/components/inline-item-editor/InlineItemEditor'
import { StopPropagation } from 'uiSrc/components/virtual-table'
import { getColumnWidth } from 'uiSrc/components/virtual-grid'
import { decompressingBuffer, getCompressor } from 'uiSrc/utils/decompressors'

import {
SetListElementDto,
SetListElementResponse,
Expand Down Expand Up @@ -276,9 +280,10 @@ const ListDetails = (props: Props) => {
expanded: boolean = false,
rowIndex = 0
) {
const { value: decompressedElementItem } = decompressingBuffer(elementItem)
const element = bufferToString(elementItem)
const tooltipContent = formatLongName(element)
const { value, isValid } = formattingBuffer(elementItem, viewFormatProp, { expanded })
const { value, isValid } = formattingBuffer(decompressedElementItem, viewFormatProp, { expanded })

if (index === editingIndex) {
const disabled = !isNonUnicodeFormatter(viewFormat, isValid)
Expand Down Expand Up @@ -350,7 +355,7 @@ const ListDetails = (props: Props) => {
>
{!expanded && (
<EuiToolTip
title={isValid ? 'Element' : `Failed to convert to ${viewFormatProp}`}
title={isValid ? 'Element' : TEXT_FAILED_CONVENT_FORMATTER(viewFormatProp)}
className={styles.tooltip}
position="bottom"
content={tooltipContent}
Expand All @@ -374,11 +379,13 @@ const ListDetails = (props: Props) => {
maxWidth: 60,
absoluteWidth: 60,
render: function Actions(_element: any, { index, element }: IListElement) {
const isEditable = isFormatEditable(viewFormat)
const compressor = getCompressor(element)
const isEditable = !compressor && isFormatEditable(viewFormat)
const tooltipContent = compressor ? TEXT_DISABLED_COMPRESSED_VALUE : TEXT_DISABLED_FORMATTER_EDITING
return (
<StopPropagation>
<div className="value-table-actions">
<EuiToolTip content={!isEditable ? TEXT_DISABLED_FORMATTER_EDITING : null}>
<EuiToolTip content={!isEditable ? tooltipContent : null} data-testid="list-edit-tooltip">
<EuiButtonIcon
iconType="pencil"
aria-label="Edit element"
Expand Down
Loading