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',