Skip to content

Commit 09f2545

Browse files
authored
Merge pull request #3624 from RedisInsight/fe/feature/RI-5917-hide-ttl-for-individual-hash-fields
RI-5917-ShowTTL-checkbox-for-hash-fields
2 parents 9fb3fdd + 4a69cc2 commit 09f2545

File tree

21 files changed

+275
-28
lines changed

21 files changed

+275
-28
lines changed

redisinsight/ui/src/components/divider/Divider.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,18 @@ export interface Props {
99
orientation?: 'horizontal' | 'vertical',
1010
variant? : 'fullWidth' | 'middle' | 'half';
1111
className?: string;
12+
style?: any
1213
}
1314

14-
const Divider = ({ orientation, variant, className, color, colorVariable }: Props) => (
15+
const Divider = ({ orientation, variant, className, color, colorVariable, ...props }: Props) => (
1516
<div
1617
className={cx(
1718
styles.divider,
1819
styles[`divider-${variant || 'fullWidth'}`],
1920
styles[`divider-${orientation || 'horizontal'}`],
2021
className,
2122
)}
23+
{...props}
2224
>
2325
<hr style={(color || colorVariable) ? { backgroundColor: color ?? `var(--${colorVariable})` } : {}} />
2426
</div>

redisinsight/ui/src/pages/browser/modules/key-details-header/KeyDetailsHeader.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ import AutoSizer from 'react-virtualized-auto-sizer'
1313
import { GroupBadge, AutoRefresh, FullScreen } from 'uiSrc/components'
1414
import {
1515
HIDE_LAST_REFRESH,
16-
KeyTypes,
17-
ModulesKeyTypes,
1816
} from 'uiSrc/constants'
1917
import {
2018
deleteSelectedKeyAction,
@@ -30,7 +28,6 @@ import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'
3028
import { RedisResponseBuffer } from 'uiSrc/slices/interfaces'
3129
import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
3230

33-
import { KeyDetailsHeaderFormatter } from './components/key-details-header-formatter'
3431
import { KeyDetailsHeaderName } from './components/key-details-header-name'
3532
import { KeyDetailsHeaderTTL } from './components/key-details-header-ttl'
3633
import { KeyDetailsHeaderDelete } from './components/key-details-header-delete'
@@ -39,7 +36,6 @@ import { KeyDetailsHeaderSizeLength } from './components/key-details-header-size
3936
import styles from './styles.module.scss'
4037

4138
export interface KeyDetailsHeaderProps {
42-
keyType: KeyTypes | ModulesKeyTypes
4339
onCloseKey: (key: RedisResponseBuffer) => void
4440
onRemoveKey: () => void
4541
onEditKey: (key: RedisResponseBuffer, newKey: RedisResponseBuffer, onFailure?: () => void) => void
@@ -56,7 +52,6 @@ const KeyDetailsHeader = ({
5652
onCloseKey,
5753
onRemoveKey,
5854
onEditKey,
59-
keyType,
6055
Actions,
6156
}: KeyDetailsHeaderProps) => {
6257
const { refreshing, loading, lastRefreshTime, isRefreshDisabled } = useSelector(selectedKeySelector)
@@ -177,9 +172,6 @@ const KeyDetailsHeader = ({
177172
onChangeAutoRefreshRate={handleChangeAutoRefreshRate}
178173
testid="key"
179174
/>
180-
{Object.values(KeyTypes).includes(keyType as KeyTypes) && (
181-
<KeyDetailsHeaderFormatter width={width} />
182-
)}
183175
{!isUndefined(Actions) && <Actions width={width} />}
184176
<KeyDetailsHeaderDelete onDelete={handleDeleteKey} />
185177
</div>

redisinsight/ui/src/pages/browser/modules/key-details/components/dynamic-type-details/DynamicTypeDetails.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { StreamDetails } from '../stream-details'
1414
export interface Props extends KeyDetailsHeaderProps {
1515
onOpenAddItemPanel: () => void
1616
onCloseAddItemPanel: () => void
17+
keyType: KeyTypes | ModulesKeyTypes
1718
}
1819

1920
const DynamicTypeDetails = (props: Props) => {
Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,97 @@
11
import React from 'react'
22
import { instance, mock } from 'ts-mockito'
3-
import { render } from 'uiSrc/utils/test-utils'
3+
import { render, screen, fireEvent } from 'uiSrc/utils/test-utils'
4+
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
5+
import { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers'
46
import { Props, HashDetails } from './HashDetails'
57

68
const mockedProps = mock<Props>()
79

10+
jest.mock('uiSrc/telemetry', () => ({
11+
...jest.requireActual('uiSrc/telemetry'),
12+
sendEventTelemetry: jest.fn(),
13+
}))
14+
15+
jest.mock('uiSrc/slices/instances/instances', () => ({
16+
...jest.requireActual('uiSrc/slices/instances/instances'),
17+
connectedInstanceOverviewSelector: jest.fn().mockReturnValue({
18+
version: '7.4.2',
19+
}),
20+
}))
21+
22+
jest.mock('uiSrc/slices/app/features', () => ({
23+
...jest.requireActual('uiSrc/slices/app/features'),
24+
appFeatureFlagsFeaturesSelector: jest.fn().mockReturnValue({
25+
hashFieldExpiration: { flag: true },
26+
}),
27+
}))
28+
829
describe('HashDetails', () => {
30+
beforeEach(() => {
31+
jest.clearAllMocks()
32+
})
933
it('should render', () => {
1034
expect(render(<HashDetails {...instance(mockedProps)} />)).toBeTruthy()
1135
})
36+
37+
it('should render subheader', () => {
38+
render(<HashDetails {...instance(mockedProps)} />)
39+
expect(screen.getByTestId('select-format-key-value')).toBeInTheDocument()
40+
})
41+
42+
it('opens and closes the add item panel', () => {
43+
render(
44+
<HashDetails
45+
{...instance(mockedProps)}
46+
onOpenAddItemPanel={() => {}}
47+
onCloseAddItemPanel={() => {}}
48+
/>
49+
)
50+
fireEvent.click(screen.getByTestId('add-key-value-items-btn'))
51+
expect(screen.getByText('Save')).toBeInTheDocument()
52+
fireEvent.click(screen.getByText('Cancel'))
53+
expect(screen.queryByText('Save')).not.toBeInTheDocument()
54+
})
55+
56+
describe('when hashFieldFeatureFlag and version higher 7.3', () => {
57+
it('renders subheader with checkbox', () => {
58+
render(<HashDetails {...instance(mockedProps)} />)
59+
expect(screen.getByText('Show TTL')).toBeInTheDocument()
60+
})
61+
62+
it('toggles the show TTL button', () => {
63+
render(<HashDetails {...instance(mockedProps)} />)
64+
const el = screen.getByTestId('test-check-ttl') as HTMLInputElement
65+
expect(el.checked).toBe(true)
66+
fireEvent.click(el)
67+
expect(el.checked).toBe(false)
68+
})
69+
70+
it('should call proper telemetry event after click on showTtl', () => {
71+
const sendEventTelemetryMock = jest.fn();
72+
(sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock)
73+
74+
render(<HashDetails {...instance(mockedProps)} />)
75+
76+
fireEvent.click(screen.getByTestId('test-check-ttl'))
77+
78+
expect(sendEventTelemetry).toBeCalledWith({
79+
event: TelemetryEvent.SHOW_HASH_TTL_CLICKED,
80+
eventData: {
81+
databaseId: INSTANCE_ID_MOCK,
82+
action: 'hide'
83+
}
84+
})
85+
86+
fireEvent.click(screen.getByTestId('test-check-ttl'))
87+
88+
expect(sendEventTelemetry).toBeCalledWith({
89+
event: TelemetryEvent.SHOW_HASH_TTL_CLICKED,
90+
eventData: {
91+
databaseId: INSTANCE_ID_MOCK,
92+
action: 'show'
93+
}
94+
})
95+
})
96+
})
1297
})

redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/HashDetails.tsx

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import React, { useState } from 'react'
22
import { useSelector } from 'react-redux'
33
import cx from 'classnames'
44

5+
import { useParams } from 'react-router-dom'
6+
import { EuiCheckbox, EuiFlexItem } from '@elastic/eui'
57
import {
68
selectedKeySelector,
79
} from 'uiSrc/slices/browser/keys'
@@ -12,9 +14,13 @@ import { isVersionHigherOrEquals } from 'uiSrc/utils'
1214
import { CommandsVersions } from 'uiSrc/constants/commandsVersions'
1315
import { connectedInstanceOverviewSelector } from 'uiSrc/slices/instances/instances'
1416
import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'
17+
import { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'
18+
import Divider from 'uiSrc/components/divider/Divider'
1519
import AddHashFields from './add-hash-fields/AddHashFields'
1620
import { HashDetailsTable } from './hash-details-table'
21+
import { KeyDetailsSubheader } from '../key-details-subheader/KeyDetailsSubheader'
1722
import { AddItemsAction } from '../key-details-actions'
23+
import styles from './styles.module.scss'
1824

1925
export interface Props extends KeyDetailsHeaderProps {
2026
onRemoveKey: () => void
@@ -28,11 +34,13 @@ const HashDetails = (props: Props) => {
2834

2935
const { loading } = useSelector(selectedKeySelector)
3036
const { version } = useSelector(connectedInstanceOverviewSelector)
37+
const { instanceId } = useParams<{ instanceId: string }>()
3138
const {
3239
[FeatureFlags.hashFieldExpiration]: hashFieldExpirationFeature
3340
} = useSelector(appFeatureFlagsFeaturesSelector)
3441

3542
const [isAddItemPanelOpen, setIsAddItemPanelOpen] = useState<boolean>(false)
43+
const [showTtl, setShowTtl] = useState<boolean>(true)
3644

3745
const isExpireFieldsAvailable = hashFieldExpirationFeature?.flag
3846
&& isVersionHigherOrEquals(version, CommandsVersions.HASH_TTL.since)
@@ -50,21 +58,55 @@ const HashDetails = (props: Props) => {
5058
}
5159

5260
const Actions = ({ width }: { width: number }) => (
53-
<AddItemsAction title="Add Fields" width={width} openAddItemPanel={openAddItemPanel} />
61+
<>
62+
{isExpireFieldsAvailable && (
63+
<>
64+
<EuiCheckbox
65+
id="showTtl"
66+
name="showTtl"
67+
label="Show TTL"
68+
className={styles.showTtlCheckbox}
69+
checked={showTtl}
70+
onChange={(e) => handleSelectShow(e.target.checked)}
71+
data-testId="test-check-ttl"
72+
/>
73+
<Divider
74+
className={styles.divider}
75+
colorVariable="separatorColor"
76+
orientation="vertical"
77+
/>
78+
</>
79+
)}
80+
<AddItemsAction title="Add Fields" width={width} openAddItemPanel={openAddItemPanel} />
81+
</>
5482
)
5583

84+
const handleSelectShow = (show: boolean) => {
85+
setShowTtl(show)
86+
87+
sendEventTelemetry({
88+
event: TelemetryEvent.SHOW_HASH_TTL_CLICKED,
89+
eventData: {
90+
databaseId: instanceId,
91+
action: show ? 'show' : 'hide'
92+
}
93+
})
94+
}
95+
5696
return (
5797
<div className="fluid flex-column relative">
5898
<KeyDetailsHeader
5999
{...props}
60100
key="key-details-header"
101+
/>
102+
<KeyDetailsSubheader
61103
keyType={keyType}
62104
Actions={Actions}
63105
/>
64106
<div className="key-details-body" key="key-details-body">
65107
{!loading && (
66108
<div className="flex-column" style={{ flex: '1', height: '100%' }}>
67-
<HashDetailsTable isExpireFieldsAvailable={isExpireFieldsAvailable} onRemoveKey={onRemoveKey} />
109+
<HashDetailsTable isExpireFieldsAvailable={isExpireFieldsAvailable && showTtl} onRemoveKey={onRemoveKey} />
68110
</div>
69111
)}
70112
{isAddItemPanelOpen && (
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
.divider {
2+
margin: 0 14px;
3+
height: 20px;
4+
width: 1px;
5+
}
6+
7+
8+
.showTtlCheckbox {
9+
font-weight: 400;
10+
font-size: 14px;
11+
12+
:global(.euiCheckbox__input) {
13+
color: var(--controlsBorderColor) !important;
14+
}
15+
16+
:global(.euiCheckbox__square) {
17+
width: 18px !important;
18+
height: 18px !important;
19+
border: 1px solid var(--controlsBorderColor) !important;
20+
border-radius: 4px !important;
21+
box-shadow: none !important;
22+
}
23+
24+
:global(.euiCheckbox__label) {
25+
color: var(--controlsLabelColor) !important;
26+
}
27+
}
28+

redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/styles.module.scss

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
.actionBtn {
2-
margin-right: 12px;
32
position: relative;
43
z-index: 2;
54

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from 'react'
2+
import { instance, mock } from 'ts-mockito'
3+
import { render } from 'uiSrc/utils/test-utils'
4+
import { KeyDetailsSubheader, Props } from './KeyDetailsSubheader'
5+
6+
const mockedProps = mock<Props>()
7+
8+
describe('KeyDetailsSubheader', () => {
9+
it('should render', () => {
10+
expect(render(<KeyDetailsSubheader {...instance(mockedProps)} />)).toBeTruthy()
11+
})
12+
})
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React, { ReactElement } from 'react'
2+
3+
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'
4+
import AutoSizer from 'react-virtualized-auto-sizer'
5+
6+
import { isUndefined } from 'lodash'
7+
import Divider from 'uiSrc/components/divider/Divider'
8+
import { KeyTypes, ModulesKeyTypes } from 'uiSrc/constants'
9+
import { KeyDetailsHeaderFormatter } from '../../../key-details-header/components/key-details-header-formatter'
10+
import styles from './styles.module.scss'
11+
12+
export interface Props {
13+
keyType: KeyTypes | ModulesKeyTypes
14+
Actions?: (props: { width: number }) => ReactElement
15+
}
16+
17+
export const KeyDetailsSubheader = ({
18+
keyType,
19+
Actions,
20+
}: Props) => (
21+
<div className={styles.subheaderContainer}>
22+
<AutoSizer disableHeight>
23+
{({ width = 0 }) => (
24+
<div style={{ width }}>
25+
<EuiFlexGroup justifyContent="flexEnd" alignItems="center" gutterSize="none">
26+
{Object.values(KeyTypes).includes(keyType as KeyTypes) && (
27+
<>
28+
<EuiFlexItem className={styles.keyFormatterItem} grow={false}>
29+
<KeyDetailsHeaderFormatter width={width} />
30+
</EuiFlexItem>
31+
<Divider
32+
className={styles.divider}
33+
colorVariable="separatorColor"
34+
orientation="vertical"
35+
/>
36+
</>
37+
)}
38+
{!isUndefined(Actions) && <Actions width={width} />}
39+
</EuiFlexGroup>
40+
</div>
41+
)}
42+
</AutoSizer>
43+
</div>
44+
)
45+
46+
export default KeyDetailsSubheader
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.subheaderContainer {
2+
padding: 12px 18px 0px 18px;
3+
4+
.divider {
5+
margin: 0 14px;
6+
height: 20px;
7+
width: 1px;
8+
}
9+
10+
.actionItem {
11+
margin-left: 12px;
12+
}
13+
}
14+

0 commit comments

Comments
 (0)