Skip to content

Commit 5e9db8b

Browse files
committed
#RI-5185 - disable refresh when editing value of key
1 parent 6a510a3 commit 5e9db8b

File tree

18 files changed

+251
-43
lines changed

18 files changed

+251
-43
lines changed

redisinsight/ui/src/components/auto-refresh/AutoRefresh.spec.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,41 @@ describe('AutoRefresh', () => {
8787
expect(onRefresh).toBeCalledTimes(3)
8888
})
8989
})
90+
91+
it('should NOT call onRefresh with disabled state', async () => {
92+
const onRefresh = jest.fn()
93+
const { rerender } = render(<AutoRefresh {...instance(mockedProps)} onRefresh={onRefresh} />)
94+
95+
fireEvent.click(screen.getByTestId('auto-refresh-config-btn'))
96+
fireEvent.click(screen.getByTestId('auto-refresh-switch'))
97+
fireEvent.click(screen.getByTestId('refresh-rate'))
98+
fireEvent.change(screen.getByTestId(INLINE_ITEM_EDITOR), { target: { value: '1' } })
99+
100+
expect(screen.getByTestId(INLINE_ITEM_EDITOR)).toHaveValue('1')
101+
102+
screen.getByTestId(/apply-btn/).click()
103+
104+
await act(() => {
105+
rerender(<AutoRefresh {...instance(mockedProps)} onRefresh={onRefresh} disabled />)
106+
})
107+
108+
await act(async () => {
109+
await new Promise((r) => setTimeout(r, 1300))
110+
})
111+
expect(onRefresh).toBeCalledTimes(0)
112+
113+
await act(async () => {
114+
await new Promise((r) => setTimeout(r, 1300))
115+
})
116+
expect(onRefresh).toBeCalledTimes(0)
117+
118+
await act(() => {
119+
rerender(<AutoRefresh {...instance(mockedProps)} onRefresh={onRefresh} disabled={false} />)
120+
})
121+
122+
await act(async () => {
123+
await new Promise((r) => setTimeout(r, 1300))
124+
})
125+
expect(onRefresh).toBeCalledTimes(1)
126+
})
90127
})

redisinsight/ui/src/components/auto-refresh/AutoRefresh.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ const AutoRefresh = ({
102102
useEffect(() => {
103103
updateLastRefresh()
104104

105-
if (enableAutoRefresh && !loading) {
105+
if (enableAutoRefresh && !loading && !disabled) {
106106
intervalRefresh = setInterval(() => {
107107
if (document.hidden) return
108108

@@ -117,7 +117,7 @@ const AutoRefresh = ({
117117
}
118118

119119
return () => clearInterval(intervalRefresh)
120-
}, [enableAutoRefresh, refreshRate, loading, lastRefreshTime])
120+
}, [enableAutoRefresh, refreshRate, loading, disabled, lastRefreshTime])
121121

122122
const getLastRefreshDelta = (time:Nullable<number>) => (Date.now() - (time || 0)) / 1_000
123123

@@ -163,12 +163,12 @@ const AutoRefresh = ({
163163
}
164164

165165
return (
166-
<div className={cx(styles.container, containerClassName, { [styles.enable]: enableAutoRefresh })}>
166+
<div className={cx(styles.container, containerClassName, { [styles.enable]: !disabled && enableAutoRefresh })}>
167167
<EuiTextColor className={styles.summary}>
168168
{displayText && (
169169
<span data-testid="refresh-message-label">{`${enableAutoRefresh ? 'Auto refresh:' : 'Last refresh:'}`}</span>
170170
)}
171-
<span className={styles.time} data-testid="refresh-message">
171+
<span className={cx(styles.time, { [styles.disabled]: disabled })} data-testid="refresh-message">
172172
{` ${enableAutoRefresh ? refreshRateMessage : refreshMessage}`}
173173
</span>
174174
</EuiTextColor>
@@ -185,7 +185,7 @@ const AutoRefresh = ({
185185
disabled={loading || disabled}
186186
onClick={handleRefreshClick}
187187
onMouseEnter={updateLastRefresh}
188-
className={cx(styles.btn, { [styles.rolling]: enableAutoRefresh })}
188+
className={cx(styles.btn, { [styles.rolling]: !disabled && enableAutoRefresh })}
189189
aria-labelledby={testid?.replaceAll?.('-', ' ') || 'Refresh button'}
190190
data-testid={testid || 'refresh-btn'}
191191
/>

redisinsight/ui/src/components/auto-refresh/styles.module.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
.time {
2929
padding-right: 6px;
3030
color: var(--euiTextSubduedColor) !important;
31+
32+
&.disabled {
33+
opacity: 0.5;
34+
}
3135
}
3236

3337
.summary {

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'
3030
import { RedisResponseBuffer } from 'uiSrc/slices/interfaces'
3131
import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
3232

33-
import { resetStringValue } from 'uiSrc/slices/browser/string'
34-
import { Nullable } from 'uiSrc/utils'
3533
import { KeyDetailsHeaderFormatter } from './components/key-details-header-formatter'
3634
import { KeyDetailsHeaderName } from './components/key-details-header-name'
3735
import { KeyDetailsHeaderTTL } from './components/key-details-header-ttl'
@@ -61,7 +59,7 @@ const KeyDetailsHeader = ({
6159
keyType,
6260
Actions,
6361
}: KeyDetailsHeaderProps) => {
64-
const { loading, lastRefreshTime } = useSelector(selectedKeySelector)
62+
const { refreshing, loading, lastRefreshTime, isRefreshDisabled } = useSelector(selectedKeySelector)
6563
const {
6664
type,
6765
length,
@@ -169,7 +167,8 @@ const KeyDetailsHeader = ({
169167
<div className={styles.subtitleActionBtns}>
170168
<AutoRefresh
171169
postfix={type}
172-
loading={loading}
170+
disabled={isRefreshDisabled}
171+
loading={loading || refreshing}
173172
lastRefreshTime={lastRefreshTime}
174173
displayText={width > HIDE_LAST_REFRESH}
175174
containerClassName={styles.actionBtn}

redisinsight/ui/src/pages/browser/modules/key-details/KeyDetails.spec.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,34 @@
11
import React from 'react'
22
import { instance, mock } from 'ts-mockito'
3-
import { render, screen } from 'uiSrc/utils/test-utils'
3+
import { cloneDeep } from 'lodash'
4+
import { cleanup, mockedStore, render, screen } from 'uiSrc/utils/test-utils'
45

6+
import { defaultSelectedKeyAction, setSelectedKeyRefreshDisabled } from 'uiSrc/slices/browser/keys'
57
import KeyDetails, { Props as KeyDetailsProps } from './KeyDetails'
68

79
const mockedProps = mock<KeyDetailsProps>()
810

11+
let store: typeof mockedStore
12+
beforeEach(() => {
13+
cleanup()
14+
store = cloneDeep(mockedStore)
15+
store.clearActions()
16+
})
17+
918
describe('KeyDetails', () => {
1019
it('should render', () => {
1120
expect(render(<KeyDetails {...instance(mockedProps)} />)).toBeTruthy()
1221
})
1322

23+
it('should call proper actions after render', () => {
24+
render(<KeyDetails {...instance(mockedProps)} />)
25+
26+
expect(store.getActions()).toEqual([
27+
defaultSelectedKeyAction(),
28+
setSelectedKeyRefreshDisabled(false)
29+
])
30+
})
31+
1432
it('should render nothing when there are no keys', () => {
1533
render(<KeyDetails {...instance(mockedProps)} totalKeys={0} keysLastRefreshTime={null} />)
1634

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
keysSelector,
1010
selectedKeyDataSelector,
1111
selectedKeySelector,
12+
setSelectedKeyRefreshDisabled,
1213
} from 'uiSrc/slices/browser/keys'
1314
import { KeyTypes } from 'uiSrc/constants'
1415

@@ -57,6 +58,7 @@ const KeyDetails = (props: Props) => {
5758
// Restore key details from context in future
5859
// (selectedKey.data?.name !== keyProp)
5960
dispatch(fetchKeyInfo(keyProp))
61+
dispatch(setSelectedKeyRefreshDisabled(false))
6062
}, [keyProp])
6163

6264
useEffect(() => {

redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/hash-details-table/HashDetailsTable.spec.tsx

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import React from 'react'
22
import { instance, mock } from 'ts-mockito'
3+
import { cloneDeep } from 'lodash'
34
import { KeyValueCompressor, TEXT_DISABLED_COMPRESSED_VALUE } from 'uiSrc/constants'
45
import { hashDataSelector } from 'uiSrc/slices/browser/hash'
56
import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'
6-
import { RedisResponseBufferType } from 'uiSrc/slices/interfaces'
77
import { anyToBuffer, bufferToString } from 'uiSrc/utils'
8-
import { act, fireEvent, render, screen, waitForEuiToolTipVisible } from 'uiSrc/utils/test-utils'
8+
import { act, cleanup, fireEvent, mockedStore, render, screen, waitForEuiToolTipVisible } from 'uiSrc/utils/test-utils'
99
import { GZIP_COMPRESSED_VALUE_1, GZIP_COMPRESSED_VALUE_2, DECOMPRESSED_VALUE_STR_1, DECOMPRESSED_VALUE_STR_2 } from 'uiSrc/utils/tests/decompressors'
10+
import { setSelectedKeyRefreshDisabled } from 'uiSrc/slices/browser/keys'
1011
import { HashDetailsTable, Props } from './HashDetailsTable'
1112

1213
const mockedProps = mock<Props>()
13-
const fields: Array<{ field: RedisResponseBufferType, value: RedisResponseBufferType }> = [
14+
const fields: Array<{ field: any, value: any }> = [
1415
{ field: { type: 'Buffer', data: [49] }, value: { type: 'Buffer', data: [49, 65] } },
1516
{ field: { type: 'Buffer', data: [49, 50, 51] }, value: { type: 'Buffer', data: [49, 11] } },
1617
{ field: { type: 'Buffer', data: [50] }, value: { type: 'Buffer', data: [49, 234, 453] } },
@@ -39,6 +40,13 @@ jest.mock('uiSrc/slices/instances/instances', () => ({
3940
}),
4041
}))
4142

43+
let store: typeof mockedStore
44+
beforeEach(() => {
45+
cleanup()
46+
store = cloneDeep(mockedStore)
47+
store.clearActions()
48+
})
49+
4250
describe('HashDetailsTable', () => {
4351
it('should render', () => {
4452
expect(render(<HashDetailsTable {...instance(mockedProps)} />)).toBeTruthy()
@@ -92,8 +100,8 @@ describe('HashDetailsTable', () => {
92100
fields: [
93101
{ field: anyToBuffer(GZIP_COMPRESSED_VALUE_1), value: anyToBuffer(GZIP_COMPRESSED_VALUE_2) },
94102
]
95-
})
96-
hashDataSelector.mockImplementation(hashDataSelectorMock)
103+
});
104+
(hashDataSelector as jest.Mock).mockImplementation(hashDataSelectorMock)
97105

98106
const { queryByTestId, queryAllByTestId } = render(<HashDetailsTable {...instance(mockedProps)} />)
99107
const fieldEl = queryAllByTestId(/hash-field-/)?.[0]
@@ -112,15 +120,15 @@ describe('HashDetailsTable', () => {
112120
fields: [
113121
{ field: anyToBuffer(GZIP_COMPRESSED_VALUE_1), value: anyToBuffer(GZIP_COMPRESSED_VALUE_2) },
114122
]
115-
})
116-
hashDataSelector.mockImplementation(hashDataSelectorMock)
123+
});
124+
(hashDataSelector as jest.Mock).mockImplementation(hashDataSelectorMock);
117125

118-
connectedInstanceSelector.mockImplementation(() => ({
126+
(connectedInstanceSelector as jest.Mock).mockImplementation(() => ({
119127
compressor: KeyValueCompressor.GZIP,
120128
}))
121129

122130
const { queryByTestId } = render(<HashDetailsTable {...instance(mockedProps)} />)
123-
const editBtn = queryByTestId(/edit-hash-button/)
131+
const editBtn = queryByTestId(/edit-hash-button/)!
124132

125133
fireEvent.click(editBtn)
126134

@@ -134,4 +142,17 @@ describe('HashDetailsTable', () => {
134142
expect(queryByTestId('hash-value-editor')).not.toBeInTheDocument()
135143
})
136144
})
145+
146+
it('should disable refresh after click on edit', () => {
147+
render(<HashDetailsTable {...instance(mockedProps)} />)
148+
149+
const afterRenderActions = [...store.getActions()]
150+
151+
fireEvent.click(screen.getAllByTestId(/edit-hash-button/)[0])
152+
153+
expect(store.getActions()).toEqual([
154+
...afterRenderActions,
155+
setSelectedKeyRefreshDisabled(true)
156+
])
157+
})
137158
})

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,12 @@ import {
3737
updateHashFieldsAction,
3838
updateHashValueStateSelector,
3939
} from 'uiSrc/slices/browser/hash'
40-
import { keysSelector, selectedKeyDataSelector, selectedKeySelector } from 'uiSrc/slices/browser/keys'
40+
import {
41+
keysSelector,
42+
selectedKeyDataSelector,
43+
selectedKeySelector,
44+
setSelectedKeyRefreshDisabled
45+
} from 'uiSrc/slices/browser/keys'
4146
import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'
4247
import { RedisResponseBuffer, RedisString } from 'uiSrc/slices/interfaces'
4348
import { getBasedOnViewTypeEvent, getMatchType, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
@@ -120,6 +125,7 @@ const HashDetailsTable = (props: Props) => {
120125
setExpandedRows([])
121126
setViewFormat(viewFormatProp)
122127
setEditingIndex(null)
128+
dispatch(setSelectedKeyRefreshDisabled(false))
123129

124130
clearCache()
125131
}
@@ -165,6 +171,7 @@ const HashDetailsTable = (props: Props) => {
165171
valueItem?: RedisResponseBuffer
166172
) => {
167173
setEditingIndex(editing ? rowIndex : null)
174+
dispatch(setSelectedKeyRefreshDisabled(editing))
168175

169176
if (editing) {
170177
const value = bufferToSerializedFormat(viewFormat, valueItem, 4)

redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/list-details-table/ListDetailsTable.spec.tsx

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import React from 'react'
22
import { mock } from 'ts-mockito'
3+
import { cloneDeep } from 'lodash'
34
import { KeyValueCompressor, TEXT_DISABLED_COMPRESSED_VALUE } from 'uiSrc/constants'
45
import { listDataSelector } from 'uiSrc/slices/browser/list'
56
import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'
67
import { anyToBuffer } from 'uiSrc/utils'
7-
import { act, fireEvent, render, screen, waitForEuiToolTipVisible } from 'uiSrc/utils/test-utils'
8+
import { act, cleanup, fireEvent, mockedStore, render, screen, waitForEuiToolTipVisible } from 'uiSrc/utils/test-utils'
89
import { GZIP_COMPRESSED_VALUE_1, DECOMPRESSED_VALUE_STR_1 } from 'uiSrc/utils/tests/decompressors'
10+
import { setSelectedKeyRefreshDisabled } from 'uiSrc/slices/browser/keys'
911
import { ListDetailsTable, Props } from './ListDetailsTable'
1012

1113
const mockedProps = mock<Props>()
@@ -43,6 +45,13 @@ jest.mock('uiSrc/slices/instances/instances', () => ({
4345
}),
4446
}))
4547

48+
let store: typeof mockedStore
49+
beforeEach(() => {
50+
cleanup()
51+
store = cloneDeep(mockedStore)
52+
store.clearActions()
53+
})
54+
4655
describe('ListDetailsTable', () => {
4756
it('should render', () => {
4857
expect(render(<ListDetailsTable {...mockedProps} />)).toBeTruthy()
@@ -90,8 +99,8 @@ describe('ListDetailsTable', () => {
9099
elements: [
91100
{ element: anyToBuffer(GZIP_COMPRESSED_VALUE_1), index: 0 },
92101
]
93-
})
94-
listDataSelector.mockImplementation(listDataSelectorMock)
102+
});
103+
(listDataSelector as jest.Mock).mockImplementation(listDataSelectorMock)
95104

96105
const { queryByTestId } = render(<ListDetailsTable {...(mockedProps)} />)
97106
const elementEl = queryByTestId(/list-element-value-/)
@@ -107,15 +116,15 @@ describe('ListDetailsTable', () => {
107116
elements: [
108117
{ element: anyToBuffer(GZIP_COMPRESSED_VALUE_1), index: 0 },
109118
]
110-
})
111-
listDataSelector.mockImplementation(listDataSelectorMock)
119+
});
120+
(listDataSelector as jest.Mock).mockImplementation(listDataSelectorMock);
112121

113-
connectedInstanceSelector.mockImplementation(() => ({
122+
(connectedInstanceSelector as jest.Mock).mockImplementation(() => ({
114123
compressor: KeyValueCompressor.GZIP,
115124
}))
116125

117126
const { queryByTestId } = render(<ListDetailsTable {...(mockedProps)} />)
118-
const editBtn = queryByTestId(/edit-list-button-/)
127+
const editBtn = queryByTestId(/edit-list-button-/)!
119128

120129
fireEvent.click(editBtn)
121130

@@ -129,4 +138,19 @@ describe('ListDetailsTable', () => {
129138
expect(queryByTestId('list-value-editor')).not.toBeInTheDocument()
130139
})
131140
})
141+
142+
it('should disable refresh when editing', async () => {
143+
render(<ListDetailsTable {...mockedProps} />)
144+
145+
const afterRenderActions = [...store.getActions()]
146+
147+
await act(() => {
148+
fireEvent.click(screen.getAllByTestId(/edit-list-button/)[0])
149+
})
150+
151+
expect(store.getActions()).toEqual([
152+
...afterRenderActions,
153+
setSelectedKeyRefreshDisabled(true)
154+
])
155+
})
132156
})

redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/list-details-table/ListDetailsTable.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,12 @@ import {
4848
validateListIndex,
4949
Nullable
5050
} from 'uiSrc/utils'
51-
import { selectedKeyDataSelector, keysSelector, selectedKeySelector } from 'uiSrc/slices/browser/keys'
51+
import {
52+
selectedKeyDataSelector,
53+
keysSelector,
54+
selectedKeySelector,
55+
setSelectedKeyRefreshDisabled
56+
} from 'uiSrc/slices/browser/keys'
5257
import { NoResultsFoundText } from 'uiSrc/constants/texts'
5358
import VirtualTable from 'uiSrc/components/virtual-table/VirtualTable'
5459
import InlineItemEditor from 'uiSrc/components/inline-item-editor/InlineItemEditor'
@@ -116,6 +121,7 @@ const ListDetailsTable = (props: Props) => {
116121
setExpandedRows([])
117122
setViewFormat(viewFormatProp)
118123
setEditingIndex(null)
124+
dispatch(setSelectedKeyRefreshDisabled(false))
119125

120126
clearCache()
121127
}
@@ -132,6 +138,7 @@ const ListDetailsTable = (props: Props) => {
132138
valueItem?: RedisResponseBuffer
133139
) => {
134140
setEditingIndex(editing ? index : null)
141+
dispatch(setSelectedKeyRefreshDisabled(editing))
135142

136143
if (editing) {
137144
const value = bufferToSerializedFormat(viewFormat, valueItem, 4)

0 commit comments

Comments
 (0)