diff --git a/redisinsight/ui/src/components/virtual-table/styles.module.scss b/redisinsight/ui/src/components/virtual-table/styles.module.scss index 67328061c3..6cead85e77 100644 --- a/redisinsight/ui/src/components/virtual-table/styles.module.scss +++ b/redisinsight/ui/src/components/virtual-table/styles.module.scss @@ -183,18 +183,7 @@ $footerHeight: 38px; :global(.key-details-table) { height: calc(100% - 94px); position: relative; - &:global(.footerOpened) { - :global(.ReactVirtualized__Table__Grid) { - padding-bottom: 254px; - overflow-y: auto !important; - } - &:global(.footerOpened--short) { - :global(.ReactVirtualized__Table__Grid) { - padding-bottom: 134px; - overflow-y: auto !important; - } - } - } + :global(.ReactVirtualized__Table__row) { font-size: 13px; align-items: normal; @@ -206,6 +195,12 @@ $footerHeight: 38px; overflow: visible !important; // fix border alignment, need to investigate why this happens margin-right: 1px !important; + + &:global(.actions.singleAction) { + .tableRowCell { + padding: 4px 8px !important; + } + } } :global(.ReactVirtualized__Table__Grid) { @@ -246,8 +241,6 @@ $footerHeight: 38px; :global { .value-table-actions { - margin-right: 5px; - .editFieldBtn { margin-right: 10px; } diff --git a/redisinsight/ui/src/constants/api.ts b/redisinsight/ui/src/constants/api.ts index 5ac48cf6f8..5571396da9 100644 --- a/redisinsight/ui/src/constants/api.ts +++ b/redisinsight/ui/src/constants/api.ts @@ -44,6 +44,7 @@ enum ApiEndpoints { HASH = 'hash', HASH_FIELDS = 'hash/fields', HASH_GET_FIELDS = 'hash/get-fields', + HASH_TTL = 'hash/ttl', LIST = 'list', LIST_GET_ELEMENTS = 'list/get-elements', diff --git a/redisinsight/ui/src/constants/commandsVersions.ts b/redisinsight/ui/src/constants/commandsVersions.ts index 0b6fadc55c..6b15d279e0 100644 --- a/redisinsight/ui/src/constants/commandsVersions.ts +++ b/redisinsight/ui/src/constants/commandsVersions.ts @@ -8,4 +8,7 @@ export const CommandsVersions = { SPUBLISH_NOT_SUPPORTED: { since: '7.0', }, + HASH_TTL: { + since: '7.4' + }, } diff --git a/redisinsight/ui/src/constants/featureFlags.ts b/redisinsight/ui/src/constants/featureFlags.ts index 2d87804cf6..36825849df 100644 --- a/redisinsight/ui/src/constants/featureFlags.ts +++ b/redisinsight/ui/src/constants/featureFlags.ts @@ -4,4 +4,5 @@ export enum FeatureFlags { cloudSsoRecommendedSettings = 'cloudSsoRecommendedSettings', databaseChat = 'databaseChat', documentationChat = 'documentationChat', + hashFieldExpiration = 'hashFieldExpiration', } diff --git a/redisinsight/ui/src/pages/browser/BrowserPage.tsx b/redisinsight/ui/src/pages/browser/BrowserPage.tsx index 237ce18536..cba22499be 100644 --- a/redisinsight/ui/src/pages/browser/BrowserPage.tsx +++ b/redisinsight/ui/src/pages/browser/BrowserPage.tsx @@ -235,7 +235,6 @@ const BrowserPage = () => {
{arePanelsCollapsed && isRightPanelOpen && !isBrowserFullScreen && ( { className={styles.backBtn} data-testid="back-right-panel-btn" > - Browser + Back )}
{ )} -
+
() @@ -32,12 +32,12 @@ describe('AddKeyHash', () => { it('should render add button', () => { render() - expect(screen.getByTestId('add-new-item')).toBeTruthy() + expect(screen.getByTestId('add-item')).toBeTruthy() }) it('should render one more field name & value inputs after click add item', () => { render() - fireEvent.click(screen.getByTestId('add-new-item')) + fireEvent.click(screen.getByTestId('add-item')) expect(screen.getAllByTestId('field-name')).toHaveLength(2) expect(screen.getAllByTestId('field-value')).toHaveLength(2) @@ -55,7 +55,7 @@ describe('AddKeyHash', () => { fieldValue, { target: { value: 'val' } } ) - fireEvent.click(screen.getByLabelText(/clear item/i)) + fireEvent.click(screen.getByTestId('remove-item')) expect(fieldName).toHaveValue('') expect(fieldValue).toHaveValue('') diff --git a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyHash/AddKeyHash.tsx b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyHash/AddKeyHash.tsx index c06132e6eb..89a7f37f7f 100644 --- a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyHash/AddKeyHash.tsx +++ b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyHash/AddKeyHash.tsx @@ -1,5 +1,4 @@ import React, { ChangeEvent, FormEvent, useEffect, useRef, useState } from 'react' -import cx from 'classnames' import { useDispatch, useSelector } from 'react-redux' import { EuiButton, @@ -11,18 +10,27 @@ import { EuiFlexItem, EuiPanel, } from '@elastic/eui' +import { toNumber } from 'lodash' import { - addHashKey, addKeyStateSelector, + isVersionHigherOrEquals, + Maybe, + stringToBuffer, + validateTTLNumberForAddKey +} from 'uiSrc/utils' +import { + addHashKey, + addKeyStateSelector, } from 'uiSrc/slices/browser/keys' +import AddMultipleFields from 'uiSrc/pages/browser/components/add-multiple-fields' -import AddItemsActions from 'uiSrc/pages/browser/components/add-items-actions/AddItemsActions' +import { CommandsVersions } from 'uiSrc/constants/commandsVersions' +import { connectedInstanceOverviewSelector } from 'uiSrc/slices/instances/instances' +import { FeatureFlags } from 'uiSrc/constants' +import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features' +import { CreateHashWithExpireDto, HashFieldDto } from 'apiSrc/modules/browser/hash/dto' -import { Maybe, stringToBuffer } from 'uiSrc/utils' -import { IHashFieldState, INITIAL_HASH_FIELD_STATE } from 'uiSrc/pages/browser/modules/key-details/components/hash-details/add-hash-fields/AddHashFields' -import { CreateHashWithExpireDto } from 'apiSrc/modules/browser/hash/dto' -import { - AddHashFormConfig as config -} from '../constants/fields-config' +import { IHashFieldState, INITIAL_HASH_FIELD_STATE } from './interfaces' +import { AddHashFormConfig as config } from '../constants/fields-config' import AddKeyFooter from '../AddKeyFooter/AddKeyFooter' export interface Props { @@ -34,11 +42,19 @@ export interface Props { const AddKeyHash = (props: Props) => { const { keyName = '', keyTTL, onCancel } = props const { loading } = useSelector(addKeyStateSelector) + const { version } = useSelector(connectedInstanceOverviewSelector) + const { + [FeatureFlags.hashFieldExpiration]: hashFieldExpirationFeature + } = useSelector(appFeatureFlagsFeaturesSelector) + const [fields, setFields] = useState([{ ...INITIAL_HASH_FIELD_STATE }]) const [isFormValid, setIsFormValid] = useState(false) const lastAddedFieldName = useRef(null) const prevCountFields = useRef(0) + const isTTLAvailable = hashFieldExpirationFeature?.flag + && isVersionHigherOrEquals(version, CommandsVersions.HASH_TTL.since) + const dispatch = useDispatch() useEffect(() => { @@ -74,11 +90,21 @@ const AddKeyHash = (props: Props) => { ? { ...item, fieldName: '', - fieldValue: '' + fieldValue: '', + fieldTTL: undefined, } : item)) setFields(newState) } + const onClickRemove = ({ id }: IHashFieldState) => { + if (fields.length === 1) { + clearFieldsValues(id) + return + } + + removeField(id) + } + const handleFieldChange = ( formField: string, id: number, @@ -106,10 +132,18 @@ const AddKeyHash = (props: Props) => { const submitData = (): void => { const data: CreateHashWithExpireDto = { keyName: stringToBuffer(keyName), - fields: fields.map((item) => ({ - field: stringToBuffer(item.fieldName), - value: stringToBuffer(item.fieldValue), - })) + fields: fields.map((item) => { + const defaultFields: HashFieldDto = { + field: stringToBuffer(item.fieldName), + value: stringToBuffer(item.fieldValue), + } + + if (isTTLAvailable && item.fieldTTL) { + defaultFields.expire = toNumber(item.fieldTTL) + } + + return defaultFields + }) } if (keyTTL !== undefined) { data.expire = keyTTL @@ -118,76 +152,69 @@ const AddKeyHash = (props: Props) => { } const isClearDisabled = (item: IHashFieldState): boolean => - fields.length === 1 && !(item.fieldName.length || item.fieldValue.length) + fields.length === 1 && !(item.fieldName.length || item.fieldValue.length || item.fieldTTL?.length) return ( - { - fields.map((item, index) => ( - - - - - - - ) => - handleFieldChange( - 'fieldName', - item.id, - e.target.value - )} - inputRef={index === fields.length - 1 ? lastAddedFieldName : null} - data-testid="field-name" - /> - - - - - ) => - handleFieldChange( - 'fieldValue', - item.id, - e.target.value - )} - data-testid="field-value" - /> - - - + + {(item, index) => ( + + + + ) => + handleFieldChange('fieldName', item.id, e.target.value)} + inputRef={index === fields.length - 1 ? lastAddedFieldName : null} + data-testid="field-name" + /> + + + + + ) => + handleFieldChange('fieldValue', item.id, e.target.value)} + data-testid="field-value" + /> + + + {isTTLAvailable && ( + + + ) => + handleFieldChange('fieldTTL', item.id, validateTTLNumberForAddKey(e.target.value))} + data-testid="hash-ttl" + /> + - - - - )) - } + )} + + )} + Submit @@ -202,31 +229,27 @@ const AddKeyHash = (props: Props) => { > -
- onCancel(true)} - className="btn-cancel btn-back" - > - Cancel - -
+ onCancel(true)} + className="btn-cancel btn-back" + > + Cancel +
-
- - Add Key - -
+ + Add Key +
diff --git a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyHash/index.ts b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyHash/index.ts new file mode 100644 index 0000000000..123c60168c --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyHash/index.ts @@ -0,0 +1,3 @@ +import AddKeyHash from './AddKeyHash' + +export default AddKeyHash diff --git a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyHash/interfaces.ts b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyHash/interfaces.ts new file mode 100644 index 0000000000..09db1e5cdb --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyHash/interfaces.ts @@ -0,0 +1,12 @@ +export interface IHashFieldState { + fieldName: string + fieldValue: string + fieldTTL?: string + id: number +} + +export const INITIAL_HASH_FIELD_STATE: IHashFieldState = { + fieldName: '', + fieldValue: '', + id: 0, +} diff --git a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyList/index.ts b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyList/index.ts new file mode 100644 index 0000000000..aab1f899c1 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyList/index.ts @@ -0,0 +1,3 @@ +import AddKeyList from './AddKeyList' + +export default AddKeyList diff --git a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyReJSON/index.ts b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyReJSON/index.ts new file mode 100644 index 0000000000..e5900899e6 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyReJSON/index.ts @@ -0,0 +1,3 @@ +import AddKeyReJSON from './AddKeyReJSON' + +export default AddKeyReJSON diff --git a/redisinsight/ui/src/pages/browser/components/add-key/AddKeySet/AddKeySet.spec.tsx b/redisinsight/ui/src/pages/browser/components/add-key/AddKeySet/AddKeySet.spec.tsx index 95452ab1cb..e72d97e999 100644 --- a/redisinsight/ui/src/pages/browser/components/add-key/AddKeySet/AddKeySet.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/add-key/AddKeySet/AddKeySet.spec.tsx @@ -22,19 +22,19 @@ describe('AddKeyZset', () => { it('should render add button', () => { render() - expect(screen.getByTestId('add-new-item')).toBeTruthy() + expect(screen.getByTestId('add-item')).toBeTruthy() }) it('should render one more member input after click add item', () => { render() - fireEvent.click(screen.getByTestId('add-new-item')) + fireEvent.click(screen.getByTestId('add-item')) expect(screen.getAllByTestId('member-name')).toHaveLength(2) }) it('should remove one member input after add item & remove one', () => { render() - fireEvent.click(screen.getByTestId('add-new-item')) + fireEvent.click(screen.getByTestId('add-item')) expect(screen.getAllByTestId('member-name')).toHaveLength(2) @@ -51,7 +51,7 @@ describe('AddKeyZset', () => { memberInput, { target: { value: 'member' } } ) - fireEvent.click(screen.getByLabelText(/clear item/i)) + fireEvent.click(screen.getByTestId('remove-item')) expect(memberInput).toHaveValue('') }) diff --git a/redisinsight/ui/src/pages/browser/components/add-key/AddKeySet/AddKeySet.tsx b/redisinsight/ui/src/pages/browser/components/add-key/AddKeySet/AddKeySet.tsx index c6ef3b2ac5..34a5a30b34 100644 --- a/redisinsight/ui/src/pages/browser/components/add-key/AddKeySet/AddKeySet.tsx +++ b/redisinsight/ui/src/pages/browser/components/add-key/AddKeySet/AddKeySet.tsx @@ -1,5 +1,4 @@ import React, { ChangeEvent, FormEvent, useState, useEffect, useRef } from 'react' -import cx from 'classnames' import { useDispatch, useSelector } from 'react-redux' import { EuiButton, @@ -15,11 +14,11 @@ import { Maybe, stringToBuffer } from 'uiSrc/utils' import { addSetKey, addKeyStateSelector, } from 'uiSrc/slices/browser/keys' -import AddItemsActions from 'uiSrc/pages/browser/components/add-items-actions/AddItemsActions' -import { INITIAL_SET_MEMBER_STATE, ISetMemberState } from 'uiSrc/pages/browser/modules/key-details/components/set-details/add-set-members/AddSetMembers' +import AddMultipleFields from 'uiSrc/pages/browser/components/add-multiple-fields' import { CreateSetWithExpireDto } from 'apiSrc/modules/browser/set/dto' +import { INITIAL_SET_MEMBER_STATE, ISetMemberState } from './interfaces' import { AddSetFormConfig as config } from '../constants/fields-config' @@ -78,6 +77,15 @@ const AddKeySet = (props: Props) => { setMembers(newState) } + const onClickRemove = ({ id }: ISetMemberState) => { + if (members.length === 1) { + clearMemberValues(id) + return + } + + removeMember(id) + } + const handleMemberChange = ( formField: string, id: number, @@ -117,57 +125,37 @@ const AddKeySet = (props: Props) => { return ( - - - { - members.map((item, index) => ( - - - - - - - ) => - handleMemberChange( - 'name', - item.id, - e.target.value - )} - inputRef={index === members.length - 1 ? lastAddedMemberName : null} - disabled={loading} - data-testid="member-name" - /> - - - - - - - - )) - } - - + + {(item, index) => ( + + + + ) => + handleMemberChange( + 'name', + item.id, + e.target.value + )} + inputRef={index === members.length - 1 ? lastAddedMemberName : null} + disabled={loading} + data-testid="member-name" + /> + + + + )} + Submit diff --git a/redisinsight/ui/src/pages/browser/components/add-key/AddKeySet/index.ts b/redisinsight/ui/src/pages/browser/components/add-key/AddKeySet/index.ts new file mode 100644 index 0000000000..5b7a93d825 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/add-key/AddKeySet/index.ts @@ -0,0 +1,3 @@ +import AddKeySet from './AddKeySet' + +export default AddKeySet diff --git a/redisinsight/ui/src/pages/browser/components/add-key/AddKeySet/interfaces.ts b/redisinsight/ui/src/pages/browser/components/add-key/AddKeySet/interfaces.ts new file mode 100644 index 0000000000..03dddb3ba1 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/add-key/AddKeySet/interfaces.ts @@ -0,0 +1,9 @@ +export interface ISetMemberState { + name: string; + id: number; +} + +export const INITIAL_SET_MEMBER_STATE: ISetMemberState = { + name: '', + id: 0, +} diff --git a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyStream/index.ts b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyStream/index.ts new file mode 100644 index 0000000000..3e34e97ba9 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyStream/index.ts @@ -0,0 +1,3 @@ +import AddKeyStream from './AddKeyStream' + +export default AddKeyStream diff --git a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyString/index.ts b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyString/index.ts new file mode 100644 index 0000000000..f2c26f8d2f --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyString/index.ts @@ -0,0 +1,3 @@ +import AddKeyString from './AddKeyString' + +export default AddKeyString diff --git a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyZset/AddKeyZset.spec.tsx b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyZset/AddKeyZset.spec.tsx index d5b2839b1b..d3022ca6a0 100644 --- a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyZset/AddKeyZset.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyZset/AddKeyZset.spec.tsx @@ -53,7 +53,7 @@ describe('AddKeyZset', () => { scoreInput, { target: { value: '100q' } } ) - expect(screen.getByTestId('add-new-item')).toBeTruthy() + expect(screen.getByTestId('add-item')).toBeTruthy() }) it('should render one more member & score inputs after click add item', () => { @@ -63,7 +63,7 @@ describe('AddKeyZset', () => { scoreInput, { target: { value: '100q' } } ) - fireEvent.click(screen.getByTestId('add-new-item')) + fireEvent.click(screen.getByTestId('add-item')) expect(screen.getAllByTestId(MEMBER_NAME)).toHaveLength(2) expect(screen.getAllByTestId(MEMBER_SCORE)).toHaveLength(2) @@ -81,7 +81,7 @@ describe('AddKeyZset', () => { scoreInput, { target: { value: '100q' } } ) - fireEvent.click(screen.getByLabelText(/clear item/i)) + fireEvent.click(screen.getByTestId('remove-item')) expect(memberInput).toHaveValue('') expect(scoreInput).toHaveValue('') diff --git a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyZset/AddKeyZset.tsx b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyZset/AddKeyZset.tsx index 705098d0cb..6c55f94da0 100644 --- a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyZset/AddKeyZset.tsx +++ b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyZset/AddKeyZset.tsx @@ -1,7 +1,6 @@ import React, { ChangeEvent, FormEvent, useState, useEffect, useRef } from 'react' import { useDispatch, useSelector } from 'react-redux' import { toNumber } from 'lodash' -import cx from 'classnames' import { EuiButton, EuiFieldText, @@ -16,9 +15,12 @@ import { Maybe, stringToBuffer, validateScoreNumber } from 'uiSrc/utils' import { isNaNConvertedString } from 'uiSrc/utils/numbers' import { addZsetKey, addKeyStateSelector } from 'uiSrc/slices/browser/keys' -import AddItemsActions from 'uiSrc/pages/browser/components/add-items-actions/AddItemsActions' - -import { INITIAL_ZSET_MEMBER_STATE, IZsetMemberState } from 'uiSrc/pages/browser/modules/key-details/components/zset-details/add-zset-members/AddZsetMembers' +import AddMultipleFields from 'uiSrc/pages/browser/components/add-multiple-fields' +import { ISetMemberState } from 'uiSrc/pages/browser/components/add-key/AddKeySet/interfaces' +import { + INITIAL_ZSET_MEMBER_STATE, + IZsetMemberState +} from 'uiSrc/pages/browser/components/add-key/AddKeyZset/interfaces' import { CreateZSetWithExpireDto } from 'apiSrc/modules/browser/z-set/dto' import AddKeyFooter from '../AddKeyFooter/AddKeyFooter' import { AddZsetFormConfig as config } from '../constants/fields-config' @@ -90,6 +92,15 @@ const AddKeyZset = (props: Props) => { setMembers(newState) } + const onClickRemove = ({ id }: ISetMemberState) => { + if (members.length === 1) { + clearMemberValues(id) + return + } + + removeMember(id) + } + const handleMemberChange = ( formField: string, id: number, @@ -165,75 +176,59 @@ const AddKeyZset = (props: Props) => { return ( - { - members.map((item, index) => ( - - - - - - - ) => - handleMemberChange( - 'name', - item.id, - e.target.value - )} - inputRef={index === members.length - 1 ? lastAddedMemberName : null} - disabled={loading} - data-testid="member-name" - /> - - - - - ) => - handleMemberChange( - 'score', - item.id, - e.target.value - )} - onBlur={() => { handleScoreBlur(item) }} - disabled={loading} - data-testid="member-score" - /> - - - - - !item.score.length))} - clearItemValues={clearMemberValues} - loading={loading} - /> - - - )) - } + + {(item, index) => ( + + + + ) => + handleMemberChange( + 'name', + item.id, + e.target.value + )} + inputRef={index === members.length - 1 ? lastAddedMemberName : null} + disabled={loading} + data-testid="member-name" + /> + + + + + ) => + handleMemberChange( + 'score', + item.id, + e.target.value + )} + onBlur={() => { handleScoreBlur(item) }} + disabled={loading} + data-testid="member-score" + /> + + + + )} + + Submit diff --git a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyZset/index.ts b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyZset/index.ts new file mode 100644 index 0000000000..3268c975f8 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyZset/index.ts @@ -0,0 +1,3 @@ +import AddKeyZset from './AddKeyZset' + +export default AddKeyZset diff --git a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyZset/interfaces.ts b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyZset/interfaces.ts new file mode 100644 index 0000000000..854d552643 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyZset/interfaces.ts @@ -0,0 +1,11 @@ +export interface IZsetMemberState { + name: string + score: string + id: number +} + +export const INITIAL_ZSET_MEMBER_STATE: IZsetMemberState = { + name: '', + score: '', + id: 0, +} diff --git a/redisinsight/ui/src/pages/browser/components/add-key/styles.module.scss b/redisinsight/ui/src/pages/browser/components/add-key/styles.module.scss index a82073c3bd..499eb4e852 100644 --- a/redisinsight/ui/src/pages/browser/components/add-key/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/add-key/styles.module.scss @@ -27,6 +27,14 @@ } } +.scrollContainer { + scroll-padding-bottom: 60px; + + // move scrollbar outside + margin: 0 -16px; + padding: 0 16px; +} + .contentFields { max-width: 680px; margin: 0 auto; diff --git a/redisinsight/ui/src/pages/browser/components/add-multiple-fields/AddMultipleFields.spec.tsx b/redisinsight/ui/src/pages/browser/components/add-multiple-fields/AddMultipleFields.spec.tsx new file mode 100644 index 0000000000..62f8f1a366 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/add-multiple-fields/AddMultipleFields.spec.tsx @@ -0,0 +1,23 @@ +import React from 'react' +import { render, screen } from 'uiSrc/utils/test-utils' + +import AddMultipleFields from './AddMultipleFields' + +const testItems1 = [{ id: '0', field: '' }] +const testItems2 = [{ id: '0', field: '', value: '' }] +const testItems3 = [{ id: '0', field: 'field', value: 'val' }, { id: '1', field: '', value: '' }] + +describe('AddMultipleFields', () => { + it('should render', () => { + expect(render( + false} + onClickAdd={jest.fn()} + onClickRemove={jest.fn()} + > + {() => (
)} + + )).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/components/add-multiple-fields/AddMultipleFields.tsx b/redisinsight/ui/src/pages/browser/components/add-multiple-fields/AddMultipleFields.tsx new file mode 100644 index 0000000000..46a99d80e8 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/add-multiple-fields/AddMultipleFields.tsx @@ -0,0 +1,81 @@ +import React from 'react' +import cx from 'classnames' +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiToolTip } from '@elastic/eui' + +import styles from './styles.module.scss' + +export interface Props { + items: T[] + children: ( + item: T, + index: number + ) => React.ReactNode + isClearDisabled: (item: T) => boolean + onClickRemove: (item: T) => void + onClickAdd: () => void +} + +const AddMultipleFields = (props: Props) => { + const { + items, + children, + isClearDisabled, + onClickRemove, + onClickAdd, + } = props + + const renderItem = (child: React.ReactNode, item: T, index?: number) => ( + + + + {child} + + + + onClickRemove(item)} + data-testid="remove-item" + /> + + + + + ) + + return ( + <> + {items.map((item, index) => renderItem(children(item, index), item, index))} + + + + + + + + + + ) +} + +export default AddMultipleFields diff --git a/redisinsight/ui/src/pages/browser/components/add-multiple-fields/index.ts b/redisinsight/ui/src/pages/browser/components/add-multiple-fields/index.ts new file mode 100644 index 0000000000..eaea646e5d --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/add-multiple-fields/index.ts @@ -0,0 +1,3 @@ +import AddMultipleFields from './AddMultipleFields' + +export default AddMultipleFields diff --git a/redisinsight/ui/src/pages/browser/components/add-multiple-fields/styles.module.scss b/redisinsight/ui/src/pages/browser/components/add-multiple-fields/styles.module.scss new file mode 100644 index 0000000000..d00ee49b68 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/add-multiple-fields/styles.module.scss @@ -0,0 +1,10 @@ +.row { + margin-top: 16px; + margin-bottom: 8px; +} + +.addBtn { + color: var(--buttonDarkenTextColor) !important; + background-color: var(--buttonDarkenBgColor) !important; + border-radius: 50% !important; +} diff --git a/redisinsight/ui/src/pages/browser/components/create-redisearch-index/CreateRedisearchIndex.tsx b/redisinsight/ui/src/pages/browser/components/create-redisearch-index/CreateRedisearchIndex.tsx index 5ebb4666c1..0f5e8c0edd 100644 --- a/redisinsight/ui/src/pages/browser/components/create-redisearch-index/CreateRedisearchIndex.tsx +++ b/redisinsight/ui/src/pages/browser/components/create-redisearch-index/CreateRedisearchIndex.tsx @@ -22,7 +22,6 @@ import React, { ChangeEvent, useEffect, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import Divider from 'uiSrc/components/divider/Divider' -import AddItemsActions from 'uiSrc/pages/browser/components/add-items-actions/AddItemsActions' import { createIndexStateSelector, createRedisearchIndexAction } from 'uiSrc/slices/browser/redisearch' import { stringToBuffer } from 'uiSrc/utils' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' @@ -30,6 +29,7 @@ import { keysSelector } from 'uiSrc/slices/browser/keys' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { getFieldTypeOptions } from 'uiSrc/utils/redisearch' import { getUtmExternalLink } from 'uiSrc/utils/links' +import AddMultipleFields from 'uiSrc/pages/browser/components/add-multiple-fields' import { CreateRedisearchIndexDto } from 'apiSrc/modules/browser/redisearch/dto' import { KEY_TYPE_OPTIONS, RedisearchIndexKeyType } from './constants' @@ -97,6 +97,15 @@ const CreateRedisearchIndex = ({ onClosePanel, onCreateIndex }: Props) => { setFields((fields) => fields.map((item) => (item.id === id ? initialFieldValue(fieldTypeOptions, id) : item))) } + const onClickRemove = ({ id }: any) => { + if (fields.length === 1) { + clearFieldsValues(id) + return + } + + removeField(id) + } + const handleFieldChange = (formField: string, id: number, value: string) => { setFields((fields) => fields.map((item) => ((item.id === id) ? { ...item, [formField]: value } : item))) } @@ -239,68 +248,51 @@ const CreateRedisearchIndex = ({ onClosePanel, onCreateIndex }: Props) => { {IdentifierInfo()} - { - fields.map((item, index) => ( - - - - - - - ) => handleFieldChange( - 'identifier', - item.id, - e.target.value - )} - inputRef={index === fields.length - 1 ? lastAddedIdentifier : null} - autoComplete="off" - data-testid={`identifier-${item.id}`} - /> - - - - - handleFieldChange( - 'fieldType', - item.id, - value - )} - data-testid={`field-type-${item.id}`} - /> - - - - - - - - )) - } + + {(item, index) => ( + + + + ) => handleFieldChange( + 'identifier', + item.id, + e.target.value + )} + inputRef={index === fields.length - 1 ? lastAddedIdentifier : null} + autoComplete="off" + data-testid={`identifier-${item.id}`} + /> + + + + + handleFieldChange( + 'fieldType', + item.id, + value + )} + data-testid={`field-type-${item.id}`} + /> + + + + )} +
diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/HashDetails.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/HashDetails.tsx index 4cde0d2297..cc83fd8137 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/HashDetails.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/HashDetails.tsx @@ -5,11 +5,15 @@ import cx from 'classnames' import { selectedKeySelector, } from 'uiSrc/slices/browser/keys' -import { KeyTypes } from 'uiSrc/constants' +import { FeatureFlags, KeyTypes } from 'uiSrc/constants' import { KeyDetailsHeader, KeyDetailsHeaderProps } from 'uiSrc/pages/browser/modules' -import { HashDetailsTable } from './hash-details-table' +import { isVersionHigherOrEquals } from 'uiSrc/utils' +import { CommandsVersions } from 'uiSrc/constants/commandsVersions' +import { connectedInstanceOverviewSelector } from 'uiSrc/slices/instances/instances' +import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features' import AddHashFields from './add-hash-fields/AddHashFields' +import { HashDetailsTable } from './hash-details-table' import { AddItemsAction } from '../key-details-actions' export interface Props extends KeyDetailsHeaderProps { @@ -23,9 +27,16 @@ const HashDetails = (props: Props) => { const { onRemoveKey, onOpenAddItemPanel, onCloseAddItemPanel } = props const { loading } = useSelector(selectedKeySelector) + const { version } = useSelector(connectedInstanceOverviewSelector) + const { + [FeatureFlags.hashFieldExpiration]: hashFieldExpirationFeature + } = useSelector(appFeatureFlagsFeaturesSelector) const [isAddItemPanelOpen, setIsAddItemPanelOpen] = useState(false) + const isExpireFieldsAvailable = hashFieldExpirationFeature?.flag + && isVersionHigherOrEquals(version, CommandsVersions.HASH_TTL.since) + const openAddItemPanel = () => { setIsAddItemPanelOpen(true) onOpenAddItemPanel() @@ -53,12 +64,12 @@ const HashDetails = (props: Props) => {
{!loading && (
- +
)} {isAddItemPanelOpen && (
- +
)}
diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/add-hash-fields/AddHashFields.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/add-hash-fields/AddHashFields.spec.tsx index 12fb76afb2..a7fb36281a 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/add-hash-fields/AddHashFields.spec.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/add-hash-fields/AddHashFields.spec.tsx @@ -35,12 +35,12 @@ describe('AddHashFields', () => { it('should render add button', () => { render() - expect(screen.getByTestId('add-new-item')).toBeTruthy() + expect(screen.getByTestId('add-item')).toBeTruthy() }) it('should render one more field name & value inputs after click add item', () => { render() - fireEvent.click(screen.getByTestId('add-new-item')) + fireEvent.click(screen.getByTestId('add-item')) expect(screen.getAllByTestId(HASH_FIELD)).toHaveLength(2) expect(screen.getAllByTestId(HASH_VALUE)).toHaveLength(2) @@ -58,7 +58,7 @@ describe('AddHashFields', () => { fieldValue, { target: { value: 'val' } } ) - fireEvent.click(screen.getByLabelText(/clear item/i)) + fireEvent.click(screen.getByTestId('remove-item')) expect(fieldName).toHaveValue('') expect(fieldValue).toHaveValue('') diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/add-hash-fields/AddHashFields.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/add-hash-fields/AddHashFields.tsx index 14e505d48a..e1ae6a4f85 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/add-hash-fields/AddHashFields.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/add-hash-fields/AddHashFields.tsx @@ -10,6 +10,7 @@ import { EuiFieldText, EuiPanel, } from '@elastic/eui' +import { toNumber } from 'lodash' import { selectedKeyDataSelector, keysSelector } from 'uiSrc/slices/browser/keys' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { @@ -19,36 +20,28 @@ import { } from 'uiSrc/slices/browser/hash' import { KeyTypes } from 'uiSrc/constants' import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import AddItemsActions from 'uiSrc/pages/browser/components/add-items-actions/AddItemsActions' -import { stringToBuffer } from 'uiSrc/utils' -import { AddFieldsToHashDto } from 'apiSrc/modules/browser/hash/dto' -import styles from '../styles.module.scss' +import { stringToBuffer, validateTTLNumberForAddKey } from 'uiSrc/utils' +import AddMultipleFields from 'uiSrc/pages/browser/components/add-multiple-fields' +import { IHashFieldState, INITIAL_HASH_FIELD_STATE } from 'uiSrc/pages/browser/components/add-key/AddKeyHash/interfaces' +import { AddFieldsToHashDto, HashFieldDto } from 'apiSrc/modules/browser/hash/dto' + +import styles from './styles.module.scss' export interface Props { + isExpireFieldsAvailable?: boolean closePanel: (isCancelled?: boolean) => void } -export interface IHashFieldState { - fieldName: string; - fieldValue: string; - id: number; -} - -export const INITIAL_HASH_FIELD_STATE: IHashFieldState = { - fieldName: '', - fieldValue: '', - id: 0, -} - const AddHashFields = (props: Props) => { - const { closePanel } = props + const { isExpireFieldsAvailable, closePanel } = props const dispatch = useDispatch() const [fields, setFields] = useState([{ ...INITIAL_HASH_FIELD_STATE }]) const { loading } = useSelector(updateHashValueStateSelector) const { name: selectedKey = '' } = useSelector(selectedKeyDataSelector) ?? { name: undefined } const { viewType } = useSelector(keysSelector) const { id: instanceId } = useSelector(connectedInstanceSelector) + const lastAddedFieldName = useRef(null) useEffect(() => @@ -85,11 +78,21 @@ const AddHashFields = (props: Props) => { ...item, fieldName: '', fieldValue: '', + fieldTTL: undefined, } : item)) setFields(newState) } + const onClickRemove = ({ id }: IHashFieldState) => { + if (fields.length === 1) { + clearFieldsValues(id) + return + } + + removeField(id) + } + const onSuccessAdded = () => { closePanel() sendEventTelemetry({ @@ -122,16 +125,24 @@ const AddHashFields = (props: Props) => { const submitData = (): void => { const data: AddFieldsToHashDto = { keyName: selectedKey, - fields: fields.map((item) => ({ - field: stringToBuffer(item.fieldName), - value: stringToBuffer(item.fieldValue,) - })), + fields: fields.map((item) => { + const defaultFields: HashFieldDto = { + field: stringToBuffer(item.fieldName), + value: stringToBuffer(item.fieldValue), + } + + if (isExpireFieldsAvailable && item.fieldTTL) { + defaultFields.expire = toNumber(item.fieldTTL) + } + + return defaultFields + }) } dispatch(addHashFieldsAction(data, onSuccessAdded)) } const isClearDisabled = (item: IHashFieldState): boolean => - fields.length === 1 && !(item.fieldName.length || item.fieldValue.length) + fields.length === 1 && !(item.fieldName.length || item.fieldValue.length || item.fieldTTL?.length) return ( <> @@ -140,59 +151,67 @@ const AddHashFields = (props: Props) => { hasShadow={false} borderRadius="none" data-test-subj="add-hash-field-panel" - className={cx('eui-yScroll', 'flexItemNoFullWidth', 'inlineFieldsNoSpace')} + className={cx(styles.container, 'eui-yScroll', 'flexItemNoFullWidth', 'inlineFieldsNoSpace')} > - {fields.map((item, index) => ( - - - - - - - ) => - handleFieldChange('fieldName', item.id, e.target.value)} - inputRef={index === fields.length - 1 ? lastAddedFieldName : null} - data-testid="hash-field" - /> - - - - - ) => - handleFieldChange('fieldValue', item.id, e.target.value)} - data-testid="hash-value" - /> - - - + + {(item, index) => ( + + + + ) => + handleFieldChange('fieldName', item.id, e.target.value)} + inputRef={index === fields.length - 1 ? lastAddedFieldName : null} + data-testid="hash-field" + /> + - + + + ) => + handleFieldChange('fieldValue', item.id, e.target.value)} + data-testid="hash-value" + /> + + + {isExpireFieldsAvailable && ( + + + ) => + handleFieldChange('fieldTTL', item.id, validateTTLNumberForAddKey(e.target.value))} + data-testid="hash-ttl" + /> + + + )} - - ))} + )} + () -const fields: Array<{ field: any, value: any }> = [ +const fields: Array<{ field: any, value: any, expire?: number }> = [ { field: { type: 'Buffer', data: [49] }, value: { type: 'Buffer', data: [49, 65] } }, { field: { type: 'Buffer', data: [49, 50, 51] }, value: { type: 'Buffer', data: [49, 11] } }, - { field: { type: 'Buffer', data: [50] }, value: { type: 'Buffer', data: [49, 234, 453] } }, + { field: { type: 'Buffer', data: [50] }, value: { type: 'Buffer', data: [49, 234, 453] }, expire: 300 }, ] jest.mock('uiSrc/slices/browser/hash', () => { @@ -114,6 +114,30 @@ describe('HashDetailsTable', () => { ]) }) + it('should not render ttl column', async () => { + render() + expect(screen.queryByTestId('hash-ttl_content-value-2')).not.toBeInTheDocument() + }) + + it('should render ttl column', async () => { + render() + + const afterRenderActions = [...store.getActions()] + + act(() => { + fireEvent.mouseEnter(screen.getByTestId('hash-ttl_content-value-2')) + }) + + act(() => { + fireEvent.click(screen.getByTestId('hash-ttl_edit-btn-2')) + }) + + expect(store.getActions()).toEqual([ + ...afterRenderActions, + setSelectedKeyRefreshDisabled(true) + ]) + }) + describe('decompressed data', () => { it('should render decompressed GZIP data', () => { const defaultState = jest.requireActual('uiSrc/slices/browser/hash').initialState diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/hash-details-table/HashDetailsTable.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/hash-details-table/HashDetailsTable.tsx index a16ebf9b24..28241b7c6d 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/hash-details-table/HashDetailsTable.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/hash-details-table/HashDetailsTable.tsx @@ -4,7 +4,7 @@ import React, { Ref, useCallback, useEffect, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { CellMeasurerCache } from 'react-virtualized' -import { isNumber } from 'lodash' +import { isNumber, toNumber } from 'lodash' import { getColumnWidth } from 'uiSrc/components/virtual-grid' import { StopPropagation } from 'uiSrc/components/virtual-table' import { @@ -34,7 +34,7 @@ import { fetchMoreHashFields, hashDataSelector, hashSelector, - updateHashFieldsAction, + updateHashFieldsAction, updateHashTTLAction, updateHashValueStateSelector, } from 'uiSrc/slices/browser/hash' import { @@ -47,7 +47,6 @@ import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { RedisResponseBuffer, RedisString } from 'uiSrc/slices/interfaces' import { getBasedOnViewTypeEvent, getMatchType, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { - bufferToSerializedFormat, bufferToString, createDeleteFieldHeader, createDeleteFieldMessage, @@ -57,13 +56,20 @@ import { isFormatEditable, isNonUnicodeFormatter, Nullable, - stringToSerializedBufferFormat + stringToSerializedBufferFormat, + truncateNumberToDuration, + validateTTLNumber } from 'uiSrc/utils' import { stringToBuffer } from 'uiSrc/utils/formatters/bufferFormatters' import { decompressingBuffer } from 'uiSrc/utils/decompressors' import PopoverDelete from 'uiSrc/pages/browser/components/popover-delete/PopoverDelete' -import { EditableTextArea } from 'uiSrc/pages/browser/modules/key-details/shared' -import { AddFieldsToHashDto, GetHashFieldsResponse, HashFieldDto, } from 'apiSrc/modules/browser/hash/dto' +import { EditableInput, EditableTextArea } from 'uiSrc/pages/browser/modules/key-details/shared' +import { + AddFieldsToHashDto, + GetHashFieldsResponse, + HashFieldDto, + UpdateHashFieldsTtlDto, +} from 'apiSrc/modules/browser/hash/dto' import styles from './styles.module.scss' @@ -80,12 +86,12 @@ const cellCache = new CellMeasurerCache({ interface IHashField extends HashFieldDto {} export interface Props { - isFooterOpen?: boolean + isExpireFieldsAvailable?: boolean onRemoveKey: () => void } const HashDetailsTable = (props: Props) => { - const { onRemoveKey } = props + const { isExpireFieldsAvailable, onRemoveKey } = props const { total, @@ -103,7 +109,7 @@ const HashDetailsTable = (props: Props) => { const [match, setMatch] = useState>(matchAllValue) const [deleting, setDeleting] = useState('') const [fields, setFields] = useState([]) - const [editingIndex, setEditingIndex] = useState>(null) + const [editingIndex, setEditingIndex] = useState>(null) const [width, setWidth] = useState(100) const [expandedRows, setExpandedRows] = useState([]) const [viewFormat, setViewFormat] = useState(viewFormatProp) @@ -178,22 +184,32 @@ const HashDetailsTable = (props: Props) => { } const handleEditField = useCallback(( - rowIndex: number, + index: number, editing: boolean, + field: string ) => { - setEditingIndex(editing ? rowIndex : null) + setEditingIndex(editing ? { index, field } : null) dispatch(setSelectedKeyRefreshDisabled(editing)) - clearCache(rowIndex) + clearCache(index) }, [viewFormat]) - const handleApplyEditField = (field = '', value: string, rowIndex: number) => { + const handleApplyEditValue = (field = '', value: string, rowIndex: number) => { const data: AddFieldsToHashDto = { keyName: key, fields: [{ field, value: stringToSerializedBufferFormat(viewFormat, value) }], } - dispatch(updateHashFieldsAction(data, () => handleEditField(rowIndex, false))) + dispatch(updateHashFieldsAction(data, () => handleEditField(rowIndex, false, 'value'))) + } + + const handleApplyEditExpire = (field = '', expire: string, rowIndex: number) => { + const data: UpdateHashFieldsTtlDto = { + keyName: key, + fields: [{ field, expire: expire ? toNumber(expire) : -1 }] + } + + dispatch(updateHashTTLAction(data, () => handleEditField(rowIndex, false, 'ttl'))) } const handleRemoveIconClick = () => { @@ -341,23 +357,25 @@ const HashDetailsTable = (props: Props) => { const isEditable = !isCompressed && isFormatEditable(viewFormat) const editTooltipContent = isCompressed ? TEXT_DISABLED_COMPRESSED_VALUE : TEXT_DISABLED_FORMATTER_EDITING + const isEditing = editingIndex?.field === 'value' && editingIndex?.index === rowIndex + return ( handleEditField(rowIndex, false)} - onApply={(value) => handleApplyEditField(fieldItem, value, rowIndex)} + onDecline={() => handleEditField(rowIndex, false, 'value')} + onApply={(value) => handleApplyEditValue(fieldItem, value, rowIndex)} approveText={TEXT_INVALID_VALUE} approveByValidation={(value) => formattingBuffer( stringToSerializedBufferFormat(viewFormat, value), viewFormat )?.isValid} - onEdit={(isEditing) => handleEditField(rowIndex, isEditing)} + onEdit={(isEditing) => handleEditField(rowIndex, isEditing, 'value')} editToolTipContent={!isEditable ? editTooltipContent : null} onUpdateTextAreaHeight={() => clearCache(rowIndex)} field={field} @@ -384,38 +402,83 @@ const HashDetailsTable = (props: Props) => { { id: 'actions', label: '', - headerClassName: 'value-table-header-actions', - className: 'actions', - absoluteWidth: 64, - minWidth: 64, - maxWidth: 64, + className: 'actions singleAction', + absoluteWidth: 40, + minWidth: 40, + maxWidth: 40, render: function Actions(_act: any, { field: fieldItem, value: valueItem }: HashFieldDto, _) { const field = bufferToString(fieldItem, viewFormat) return ( -
- -
+
) }, }, ] + if (isExpireFieldsAvailable) { + columns.splice(2, 0, { + id: 'ttl', + label: 'TTL', + absoluteWidth: 140, + minWidth: 140, + truncateText: true, + className: 'noPadding', + render: function TTL( + _name: string, + { field: fieldItem, expire }: IHashField, + _expanded?: boolean, + rowIndex = 0 + ) { + const field = bufferToString(fieldItem, viewFormat) + const isEditing = editingIndex?.field === 'ttl' && editingIndex?.index === rowIndex + + return ( + handleEditField(rowIndex, value, 'ttl')} + onDecline={() => handleEditField(rowIndex, false, 'ttl')} + onApply={(value) => handleApplyEditExpire(fieldItem, value, 'ttl')} + testIdPrefix="hash-ttl" + validation={validateTTLNumber} + > +
+ {expire === -1 ? 'No Limit' : ( + + <>{expire} + + )} +
+
+ ) + } + }) + } + return ( <>
() @@ -54,7 +54,7 @@ describe('AddZsetMembers', () => { memberInput, { target: { value: 'member' } } ) - fireEvent.click(screen.getByLabelText(/clear item/i)) + fireEvent.click(screen.getByTestId('remove-item')) expect(memberInput).toHaveValue('') }) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/add-set-members/AddSetMembers.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/add-set-members/AddSetMembers.tsx index 2762f3481a..dcd5a7b6e2 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/add-set-members/AddSetMembers.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/add-set-members/AddSetMembers.tsx @@ -18,23 +18,16 @@ import { KeyTypes } from 'uiSrc/constants' import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { stringToBuffer } from 'uiSrc/utils' -import AddItemsActions from 'uiSrc/pages/browser/components/add-items-actions/AddItemsActions' import { AddZsetFormConfig as config } from 'uiSrc/pages/browser/components/add-key/constants/fields-config' +import { INITIAL_SET_MEMBER_STATE, ISetMemberState } from 'uiSrc/pages/browser/components/add-key/AddKeySet/interfaces' +import AddMultipleFields from 'uiSrc/pages/browser/components/add-multiple-fields' + +import styles from './styles.module.scss' export interface Props { closePanel: (isCancelled?: boolean) => void } -export interface ISetMemberState { - name: string; - id: number; -} - -export const INITIAL_SET_MEMBER_STATE: ISetMemberState = { - name: '', - id: 0, -} - const AddSetMembers = (props: Props) => { const { closePanel } = props const dispatch = useDispatch() @@ -92,6 +85,15 @@ const AddSetMembers = (props: Props) => { setMembers(newState) } + const onClickRemove = ({ id }: ISetMemberState) => { + if (members.length === 1) { + clearMemberValues(id) + return + } + + removeMember(id) + } + const handleMemberChange = (formField: string, id: number, value: string) => { const newState = members.map((item) => { if (item.id === id) { @@ -123,44 +125,35 @@ const AddSetMembers = (props: Props) => { hasShadow={false} borderRadius="none" data-test-subj="add-set-field-panel" - className={cx('eui-yScroll', 'flexItemNoFullWidth')} + className={cx(styles.container, 'eui-yScroll', 'flexItemNoFullWidth')} > - {members.map((item, index) => ( - - + + {(item, index) => ( + - - - - ) => - handleMemberChange('name', item.id, e.target.value)} - inputRef={index === members.length - 1 ? lastAddedMemberName : null} - disabled={loading} - data-testid="member-name" - /> - - - + + ) => + handleMemberChange('name', item.id, e.target.value)} + inputRef={index === members.length - 1 ? lastAddedMemberName : null} + disabled={loading} + data-testid="member-name" + /> + - - - ))} + )} + { className={cx(styles.content, 'eui-yScroll', 'flexItemNoFullWidth', 'inlineFieldsNoSpace')} > void entryID: string setEntryID: React.Dispatch> fields: any[] setFields: React.Dispatch> - handleBlurEntryID?: () => void } const MIN_ENTRY_ID_VALUE = '0-1' const StreamEntryFields = (props: Props) => { const { - compressed, entryID, setEntryID, entryIdError, @@ -81,6 +77,15 @@ const StreamEntryFields = (props: Props) => { setFields(newState) } + const onClickRemove = ({ id }: any) => { + if (fields.length === 1) { + clearFieldsValues(id) + return + } + + removeField(id) + } + const handleEntryIdChange = (e: ChangeEvent) => { setEntryID(validateEntryId(e.target.value)) } @@ -147,72 +152,57 @@ const StreamEntryFields = (props: Props) => {
- { - fields.map((item, index) => ( - - - - - - - ) => - handleFieldChange( - 'name', - item.id, - e.target.value - )} - inputRef={index === fields.length - 1 ? lastAddedFieldName : null} - autoComplete="off" - data-testid="field-name" - /> - - - - - ) => - handleFieldChange( - 'value', - item.id, - e.target.value - )} - autoComplete="off" - data-testid="field-value" - /> - - - - - - - - )) - } + + {(item, index) => ( + + + + ) => + handleFieldChange( + 'name', + item.id, + e.target.value + )} + inputRef={index === fields.length - 1 ? lastAddedFieldName : null} + autoComplete="off" + data-testid="field-name" + /> + + + + + ) => + handleFieldChange( + 'value', + item.id, + e.target.value + )} + autoComplete="off" + data-testid="field-value" + /> + + + + )} +
diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-entity/styles.module.scss b/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-entity/styles.module.scss index 927f43c80a..c7dd471fc6 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-entity/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-entity/styles.module.scss @@ -8,7 +8,7 @@ $rowHeight: 43px; border-top: 1px solid var(--euiColorPrimary); padding: 12px 20px; max-height: 234px; - scroll-padding-bottom: 30px; + scroll-padding-bottom: 60px; } .container { diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/add-zset-members/AddZsetMembers.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/add-zset-members/AddZsetMembers.spec.tsx index ab94071d33..4e400c0fe6 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/add-zset-members/AddZsetMembers.spec.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/add-zset-members/AddZsetMembers.spec.tsx @@ -1,6 +1,6 @@ import React from 'react' -import { fireEvent, render, screen } from 'uiSrc/utils/test-utils' import { instance, mock } from 'ts-mockito' +import { fireEvent, render, screen } from 'uiSrc/utils/test-utils' import AddZsetMembers, { Props } from './AddZsetMembers' const MEMBER_NAME = 'member-name' @@ -53,7 +53,7 @@ describe('AddZsetMembers', () => { scoreInput, { target: { value: '100q' } } ) - expect(screen.getByTestId('add-new-item')).toBeTruthy() + expect(screen.getByTestId('add-item')).toBeTruthy() }) it('should render one more member & score inputs after click add item', () => { @@ -63,7 +63,7 @@ describe('AddZsetMembers', () => { scoreInput, { target: { value: '100q' } } ) - fireEvent.click(screen.getByTestId('add-new-item')) + fireEvent.click(screen.getByTestId('add-item')) expect(screen.getAllByTestId(MEMBER_NAME)).toHaveLength(2) expect(screen.getAllByTestId(MEMBER_SCORE)).toHaveLength(2) @@ -81,7 +81,7 @@ describe('AddZsetMembers', () => { scoreInput, { target: { value: '100q' } } ) - fireEvent.click(screen.getByLabelText(/clear item/i)) + fireEvent.click(screen.getByTestId('remove-item')) expect(memberInput).toHaveValue('') expect(scoreInput).toHaveValue('') diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/add-zset-members/AddZsetMembers.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/add-zset-members/AddZsetMembers.tsx index aad5f7bd08..7c7a4bc74f 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/add-zset-members/AddZsetMembers.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/add-zset-members/AddZsetMembers.tsx @@ -22,24 +22,19 @@ import { } from 'uiSrc/slices/browser/zset' import { AddZsetFormConfig as config } from 'uiSrc/pages/browser/components/add-key/constants/fields-config' -import AddItemsActions from 'uiSrc/pages/browser/components/add-items-actions/AddItemsActions' +import { + INITIAL_ZSET_MEMBER_STATE, + IZsetMemberState +} from 'uiSrc/pages/browser/components/add-key/AddKeyZset/interfaces' +import AddMultipleFields from 'uiSrc/pages/browser/components/add-multiple-fields' +import { ISetMemberState } from 'uiSrc/pages/browser/components/add-key/AddKeySet/interfaces' + +import styles from './styles.module.scss' export interface Props { closePanel: (isCancelled?: boolean) => void } -export interface IZsetMemberState { - name: string - score: string - id: number -} - -export const INITIAL_ZSET_MEMBER_STATE: IZsetMemberState = { - name: '', - score: '', - id: 0, -} - const AddZsetMembers = (props: Props) => { const { closePanel } = props const dispatch = useDispatch() @@ -105,6 +100,15 @@ const AddZsetMembers = (props: Props) => { setMembers(newState) } + const onClickRemove = ({ id }: ISetMemberState) => { + if (members.length === 1) { + clearMemberValues(id) + return + } + + removeMember(id) + } + const validateScore = (value: any) => { const validatedValue = validateScoreNumber(value) return validatedValue.toString().length ? validatedValue : '' @@ -172,64 +176,54 @@ const AddZsetMembers = (props: Props) => { hasShadow={false} borderRadius="none" data-test-subj="add-zset-field-panel" - className={cx('eui-yScroll', 'flexItemNoFullWidth', 'inlineFieldsNoSpace')} + className={cx(styles.container, 'eui-yScroll', 'flexItemNoFullWidth', 'inlineFieldsNoSpace')} > - {members.map((item, index) => ( - - + + {(item, index) => ( + - - - - ) => - handleMemberChange('name', item.id, e.target.value)} - inputRef={index === members.length - 1 ? lastAddedMemberName : null} - disabled={loading} - data-testid="member-name" - /> - - - - - ) => - handleMemberChange('score', item.id, e.target.value)} - onBlur={() => { - handleScoreBlur(item) - }} - disabled={loading} - data-testid="member-score" - /> - - - + + ) => + handleMemberChange('name', item.id, e.target.value)} + inputRef={index === members.length - 1 ? lastAddedMemberName : null} + disabled={loading} + data-testid="member-name" + /> + + + + + ) => + handleMemberChange('score', item.id, e.target.value)} + onBlur={() => { + handleScoreBlur(item) + }} + disabled={loading} + data-testid="member-score" + /> + - !item.score.length)} - clearItemValues={clearMemberValues} - loading={loading} - /> - - ))} + )} +
() +const Text = () => (text) + +describe('EditableInput', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should display editor', () => { + render( + + + + ) + + expect(screen.getByTestId('inline-item-editor')).toBeInTheDocument() + }) + + it('should call on apply', () => { + const onApply = jest.fn() + render( + + + + ) + + fireEvent.change(screen.getByTestId('inline-item-editor'), { target: { value: 'value' } }) + fireEvent.click(screen.getByTestId('apply-btn')) + + expect(onApply).toBeCalledWith('value', expect.any(Object)) + }) + + it('should call on decline', () => { + const onDecline = jest.fn() + render( + + + + ) + + fireEvent.change(screen.getByTestId('inline-item-editor'), { target: { value: 'value' } }) + fireEvent.click(screen.getByTestId('cancel-btn')) + + expect(onDecline).toBeCalled() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-input/EditableInput.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-input/EditableInput.tsx index 950ccc203e..976c4ab970 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-input/EditableInput.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-input/EditableInput.tsx @@ -85,16 +85,18 @@ const EditableInput = (props: Props) => { { onDecline(event) - onEdit(false) + onEdit?.(false) }} onApply={(value, event) => { onApply(value, event) - onEdit(false) + onEdit?.(false) }} validation={validation} /> diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-input/styles.module.scss b/redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-input/styles.module.scss index 4b17c06275..38571b7009 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-input/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-input/styles.module.scss @@ -6,16 +6,34 @@ width: 100%; height: 100%; + min-height: 42px; padding-right: 32px; .editBtnAnchor { position: absolute; - top: 4px; + top: 10px; right: 4px; } } .inputWrapper { - max-width: calc(100% - 82px); + max-width: calc(100% - 48px); padding: 0 4px; } + +.controls { + padding: 2px; + width: 48px !important; + box-shadow: none !important; + background-color: transparent !important; + + display: flex; + align-items: center; + justify-content: space-between; + + :global(.euiButtonIcon), :global(.euiToolTipAnchor) { + width: 20px !important; + height: 20px !important; + min-width: 20px !important; + } +} diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-textarea/EditableTextArea.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-textarea/EditableTextArea.spec.tsx new file mode 100644 index 0000000000..a9b4545b34 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-textarea/EditableTextArea.spec.tsx @@ -0,0 +1,79 @@ +import React from 'react' +import { mock } from 'ts-mockito' +import { fireEvent, render, screen } from 'uiSrc/utils/test-utils' + +import EditableTextArea, { Props } from './EditableTextArea' + +const mockedProps = mock() +const Text = () => (text) + +describe('EditableTextArea', () => { + it('should render', () => { + expect(render( + + + + )).toBeTruthy() + }) + + it('should display editor', () => { + render( + + + + ) + + expect(screen.getByTestId('item_value-editor-field')).toBeInTheDocument() + }) + + it('should call on apply', () => { + const onApply = jest.fn() + render( + + + + ) + + fireEvent.change(screen.getByTestId('item_value-editor-field'), { target: { value: 'value' } }) + fireEvent.click(screen.getByTestId('apply-btn')) + + expect(onApply).toBeCalledWith('value', expect.any(Object)) + }) + + it('should call on decline', () => { + const onDecline = jest.fn() + render( + + + + ) + + fireEvent.change(screen.getByTestId('item_value-editor-field'), { target: { value: 'value' } }) + fireEvent.click(screen.getByTestId('cancel-btn')) + + expect(onDecline).toBeCalled() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-textarea/EditableTextArea.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-textarea/EditableTextArea.tsx index 7fadd0e46d..51e8961315 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-textarea/EditableTextArea.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-textarea/EditableTextArea.tsx @@ -21,7 +21,7 @@ export interface Props { editToolTipContent?: React.ReactNode approveByValidation?: (value: string) => boolean onEdit: (isEditing: boolean) => void - onUpdateTextAreaHeight: () => void + onUpdateTextAreaHeight?: () => void onChange?: (e: ChangeEvent) => void onDecline: (event?: React.MouseEvent) => void onApply: (value: string, event: React.MouseEvent) => void @@ -69,7 +69,7 @@ const EditableTextArea = (props: Props) => { if (textAreaRef.current) { textAreaRef.current.style.height = '0px' textAreaRef.current.style.height = `${textAreaRef.current?.scrollHeight || 0}px` - onUpdateTextAreaHeight() + onUpdateTextAreaHeight?.() } } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-textarea/styles.module.scss b/redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-textarea/styles.module.scss index 09f12b9a13..95a03f9d9a 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-textarea/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-textarea/styles.module.scss @@ -11,7 +11,7 @@ .editBtnAnchor { position: absolute; - top: 4px; + top: 10px; right: 4px; } } diff --git a/redisinsight/ui/src/pages/browser/styles.module.scss b/redisinsight/ui/src/pages/browser/styles.module.scss index e0a40ffbf2..78257fd4fb 100644 --- a/redisinsight/ui/src/pages/browser/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/styles.module.scss @@ -114,4 +114,12 @@ $breakpoint-to-hide-resize-panel: 1280px; flex-grow: 0; align-self: self-start; margin: 0 0 8px 16px; + + background-color: transparent !important; + border: 0 !important; + box-shadow: none !important; + + &:hover { + color: var(--buttonSecondaryTextColor) !important + } } diff --git a/redisinsight/ui/src/slices/app/features.ts b/redisinsight/ui/src/slices/app/features.ts index 197ca5d837..36623173fa 100644 --- a/redisinsight/ui/src/slices/app/features.ts +++ b/redisinsight/ui/src/slices/app/features.ts @@ -38,6 +38,9 @@ export const initialState: StateAppFeatures = { [FeatureFlags.databaseChat]: { flag: false }, + [FeatureFlags.hashFieldExpiration]: { + flag: false + }, } } } diff --git a/redisinsight/ui/src/slices/browser/hash.ts b/redisinsight/ui/src/slices/browser/hash.ts index f5c55adf28..646a266c93 100644 --- a/redisinsight/ui/src/slices/browser/hash.ts +++ b/redisinsight/ui/src/slices/browser/hash.ts @@ -9,6 +9,7 @@ import successMessages from 'uiSrc/components/notifications/success-messages' import { GetHashFieldsResponse, AddFieldsToHashDto, + UpdateHashFieldsTtlDto, } from 'apiSrc/modules/browser/hash/dto' import { deleteKeyFromList, @@ -151,7 +152,10 @@ const hashSlice = createSlice({ (item) => isEqualBuffers(item.field, listItem.field) ) if (index > -1) { - return payload[index] + return ({ + ...listItem, + ...payload[index] + }) } return listItem }) @@ -395,6 +399,7 @@ export function addHashFieldsAction( } } } + // Asynchronous thunk action export function updateHashFieldsAction( data: AddFieldsToHashDto, @@ -426,20 +431,76 @@ export function updateHashFieldsAction( keyType: KeyTypes.Hash, } }) - if (onSuccessAction) { - onSuccessAction() - } dispatch(updateValueSuccess()) dispatch(updateFieldsInList(data.fields)) dispatch(refreshKeyInfoAction(data.keyName)) + onSuccessAction?.() } } catch (error) { - if (onFailAction) { - onFailAction() + const errorMessage = getApiErrorMessage(error) + dispatch(addErrorNotification(error)) + dispatch(updateValueFailure(errorMessage)) + onFailAction?.() + } + } +} + +// Asynchronous thunk action +export function updateHashTTLAction( + data: UpdateHashFieldsTtlDto, + onSuccessAction?: () => void, + onFailAction?: () => void +) { + return async (dispatch: AppDispatch, stateInit: () => RootState) => { + dispatch(updateValue()) + try { + const state = stateInit() + const { encoding } = state.app.info + const { status } = await apiService.patch( + getUrl( + state.connections.instances.connectedInstance?.id, + ApiEndpoints.HASH_TTL + ), + data, + { params: { encoding } }, + ) + if (isStatusSuccessful(status)) { + sendEventTelemetry({ + event: getBasedOnViewTypeEvent( + state.browser.keys?.viewType, + TelemetryEvent.BROWSER_FIELD_TTL_EDITED, + TelemetryEvent.TREE_VIEW_FIELD_TTL_EDITED + ), + eventData: { + databaseId: state.connections.instances?.connectedInstance?.id, + } + }) + onSuccessAction?.() + dispatch(updateValueSuccess()) + + const key = data.keyName as RedisResponseBuffer + const isLastFieldAffected = state.browser.hash.data.total - data.fields.length === 0 + const isSetToZero = data.fields.reduce((prev, current) => prev + current.expire, 0) === 0 + + if (isLastFieldAffected && isSetToZero) { + dispatch(deleteSelectedKeySuccess()) + dispatch(deleteKeyFromList(key)) + dispatch(addMessageNotification(successMessages.DELETED_KEY(key))) + return + } + + if (isSetToZero) { + dispatch(removeFieldsFromList(data.fields.map(({ field }) => field) as any)) + return + } + + dispatch(updateFieldsInList(data.fields as any)) } + } catch (error) { const errorMessage = getApiErrorMessage(error) dispatch(addErrorNotification(error)) dispatch(updateValueFailure(errorMessage)) + onFailAction?.() } } } diff --git a/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss b/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss index 14122e6b49..33a7da7227 100644 --- a/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss +++ b/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss @@ -110,6 +110,8 @@ $buttonDangerToastColor: #e8524a; $buttonDangerToastHoverColor: #bf3932; $buttonGuideBgColor: #2b2b2b; $buttonSuccessColor: #5bc69b; +$buttonDarkenTextColor: #ffffff; +$buttonDarkenBgColor: #292f47; $comboBoxBadgeBgColor: #363636; $loadingContentColor: #313131; diff --git a/redisinsight/ui/src/styles/themes/dark_theme/darkTheme.scss b/redisinsight/ui/src/styles/themes/dark_theme/darkTheme.scss index 237702a637..33aa8fb7f0 100644 --- a/redisinsight/ui/src/styles/themes/dark_theme/darkTheme.scss +++ b/redisinsight/ui/src/styles/themes/dark_theme/darkTheme.scss @@ -113,6 +113,8 @@ --buttonDangerToastHoverColor: #{$buttonDangerToastHoverColor}; --buttonGuideBgColor: #{$buttonGuideBgColor}; --buttonSuccessColor: #{$buttonSuccessColor}; + --buttonDarkenTextColor: #{$buttonDarkenTextColor}; + --buttonDarkenBgColor: #{$buttonDarkenBgColor}; --comboBoxBadgeBgColor: #{$comboBoxBadgeBgColor}; --loadingContentColor: #{$loadingContentColor}; diff --git a/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss b/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss index 53351f66af..c5303bed2a 100644 --- a/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss +++ b/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss @@ -75,6 +75,8 @@ $buttonDangerToastColor: #e8524a; $buttonDangerToastHoverColor: #bf3932; $buttonGuideBgColor: transparent; $buttonSuccessColor: #408B6D; +$buttonDarkenTextColor: #3163D8; +$buttonDarkenBgColor: #D7E3FA; $comboBoxBadgeBgColor: #edf0f5; $loadingContentColor: #eef1f6; diff --git a/redisinsight/ui/src/styles/themes/light_theme/lightTheme.scss b/redisinsight/ui/src/styles/themes/light_theme/lightTheme.scss index 10a7f88631..2c17825b1e 100644 --- a/redisinsight/ui/src/styles/themes/light_theme/lightTheme.scss +++ b/redisinsight/ui/src/styles/themes/light_theme/lightTheme.scss @@ -108,6 +108,8 @@ --buttonDangerToastHoverColor: #{$buttonDangerToastHoverColor}; --buttonGuideBgColor: #{$buttonGuideBgColor}; --buttonSuccessColor: #{$buttonSuccessColor}; + --buttonDarkenTextColor: #{$buttonDarkenTextColor}; + --buttonDarkenBgColor: #{$buttonDarkenBgColor}; --comboBoxBadgeBgColor: #{$comboBoxBadgeBgColor}; --loadingContentColor: #{$loadingContentColor}; diff --git a/redisinsight/ui/src/telemetry/events.ts b/redisinsight/ui/src/telemetry/events.ts index 89b3522e70..071c592890 100644 --- a/redisinsight/ui/src/telemetry/events.ts +++ b/redisinsight/ui/src/telemetry/events.ts @@ -59,6 +59,7 @@ export enum TelemetryEvent { BROWSER_KEY_VALUE_ADDED = 'BROWSER_KEY_VALUE_ADDED', BROWSER_KEY_VALUE_REMOVED = 'BROWSER_KEY_VALUE_REMOVED', BROWSER_KEY_VALUE_EDITED = 'BROWSER_KEY_VALUE_EDITED', + BROWSER_FIELD_TTL_EDITED = 'BROWSER_FIELD_TTL_EDITED', BROWSER_JSON_PROPERTY_EDITED = 'BROWSER_JSON_PROPERTY_EDITED', BROWSER_JSON_PROPERTY_DELETED = 'BROWSER_JSON_PROPERTY_DELETED', BROWSER_JSON_PROPERTY_ADDED = 'BROWSER_JSON_PROPERTY_ADDED', @@ -143,6 +144,7 @@ export enum TelemetryEvent { TREE_VIEW_KEY_DELETE_CLICKED = 'TREE_VIEW_KEY_DELETE_CLICKED', TREE_VIEW_KEY_VALUE_REMOVED = 'TREE_VIEW_KEY_VALUE_REMOVED', TREE_VIEW_KEY_VALUE_EDITED = 'TREE_VIEW_KEY_VALUE_EDITED', + TREE_VIEW_FIELD_TTL_EDITED = 'TREE_VIEW_FIELD_TTL_EDITED', TREE_VIEW_KEY_COPIED = 'TREE_VIEW_KEY_COPIED', TREE_VIEW_JSON_KEY_EXPANDED = 'TREE_VIEW_JSON_KEY_EXPANDED', TREE_VIEW_JSON_KEY_COLLAPSED = 'TREE_VIEW_JSON_KEY_COLLAPSED',