Skip to content

Commit 0d944e8

Browse files
authored
Merge pull request #2797 from RedisInsight/bugfix/RI-5163_fix-bulk-delete_&_monitor
#RI-5163 - fix bulk action disconnection flow, monitor
2 parents a1fa3a6 + f1f1d56 commit 0d944e8

File tree

10 files changed

+167
-72
lines changed

10 files changed

+167
-72
lines changed

redisinsight/ui/src/components/bulk-actions-config/BulkActionsConfig.spec.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import React from 'react'
44
import MockedSocket from 'socket.io-mock'
55
import socketIO from 'socket.io-client'
66
import { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils'
7-
import { BulkActionsServerEvent, BulkActionsType, SocketEvent } from 'uiSrc/constants'
7+
import { BulkActionsServerEvent, BulkActionsStatus, BulkActionsType, SocketEvent } from 'uiSrc/constants'
88
import {
99
bulkActionsDeleteSelector,
1010
bulkActionsSelector,
1111
disconnectBulkDeleteAction,
1212
setBulkActionConnected,
13-
setBulkDeleteLoading
13+
setBulkDeleteLoading, setDeleteOverviewStatus
1414
} from 'uiSrc/slices/browser/bulkActions'
1515
import BulkActionsConfig from './BulkActionsConfig'
1616

@@ -110,6 +110,7 @@ describe('BulkActionsConfig', () => {
110110
const afterRenderActions = [
111111
setBulkActionConnected(true),
112112
setBulkDeleteLoading(true),
113+
setDeleteOverviewStatus(BulkActionsStatus.Disconnected),
113114
disconnectBulkDeleteAction(),
114115
]
115116
expect(store.getActions()).toEqual([...afterRenderActions])

redisinsight/ui/src/components/bulk-actions-config/BulkActionsConfig.tsx

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
setDeleteOverview,
1212
setBulkActionsInitialState,
1313
bulkActionsDeleteSelector,
14+
setDeleteOverviewStatus,
1415
} from 'uiSrc/slices/browser/bulkActions'
1516
import { getBaseApiUrl, Nullable } from 'uiSrc/utils'
1617
import { sessionStorageService } from 'uiSrc/services'
@@ -21,11 +22,7 @@ import { BrowserStorageItem, BulkActionsServerEvent, BulkActionsStatus, BulkActi
2122
import { addErrorNotification } from 'uiSrc/slices/app/notifications'
2223
import { CustomHeaders } from 'uiSrc/constants/api'
2324

24-
interface IProps {
25-
retryDelay?: number
26-
}
27-
28-
const BulkActionsConfig = ({ retryDelay = 5000 } : IProps) => {
25+
const BulkActionsConfig = () => {
2926
const { id: instanceId = '', db } = useSelector(connectedInstanceSelector)
3027
const { isConnected } = useSelector(bulkActionsSelector)
3128
const { isActionTriggered: isDeleteTriggered } = useSelector(bulkActionsDeleteSelector)
@@ -60,11 +57,8 @@ const BulkActionsConfig = ({ retryDelay = 5000 } : IProps) => {
6057

6158
// Catch disconnect
6259
socketRef.current?.on(SocketEvent.Disconnect, () => {
63-
if (retryDelay) {
64-
retryTimer = setTimeout(handleDisconnect, retryDelay)
65-
} else {
66-
handleDisconnect()
67-
}
60+
dispatch(setDeleteOverviewStatus(BulkActionsStatus.Disconnected))
61+
handleDisconnect()
6862
})
6963
}, [instanceId, isDeleteTriggered])
7064

@@ -147,10 +141,8 @@ const BulkActionsConfig = ({ retryDelay = 5000 } : IProps) => {
147141
const onBulkDeleteAborted = (data: any) => {
148142
dispatch(setBulkDeleteLoading(false))
149143
sessionStorageService.set(BrowserStorageItem.bulkActionDeleteId, '')
150-
151-
if (data.status === 'aborted') {
152-
dispatch(setDeleteOverview(data))
153-
}
144+
dispatch(setDeleteOverview(data))
145+
handleDisconnect()
154146
}
155147

156148
useEffect(() => {

redisinsight/ui/src/components/monitor-config/MonitorConfig.spec.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
pauseMonitor,
1010
setSocket,
1111
stopMonitor,
12-
lockResume
12+
lockResume, setLogFileId, setStartTimestamp
1313
} from 'uiSrc/slices/cli/monitor'
1414
import { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils'
1515
import { MonitorEvent, SocketEvent } from 'uiSrc/constants'
@@ -69,26 +69,47 @@ describe('MonitorConfig', () => {
6969
it(`should emit ${MonitorEvent.Monitor} event`, () => {
7070
const monitorSelectorMock = jest.fn().mockReturnValue({
7171
isRunning: true,
72+
isSaveToFile: true
7273
})
7374
monitorSelector.mockImplementation(monitorSelectorMock)
7475

7576
const { unmount } = render(<MonitorConfig />)
7677

77-
socket.on(MonitorEvent.MonitorData, (data: []) => {
78-
expect(data).toEqual(['message1', 'message2'])
78+
socket.socketClient.on(MonitorEvent.Monitor, (data: any) => {
79+
expect(data).toEqual({ logFileId: expect.any(String) })
7980
})
8081

81-
socket.socketClient.emit(MonitorEvent.MonitorData, ['message1', 'message2'])
82+
socket.socketClient.emit(SocketEvent.Connect)
8283

8384
const afterRenderActions = [
8485
setSocket(socket),
85-
setMonitorLoadingPause(true)
86+
setMonitorLoadingPause(true),
87+
setLogFileId(expect.any(String)),
88+
setStartTimestamp(expect.any(Number))
8689
]
8790
expect(store.getActions()).toEqual([...afterRenderActions])
8891

8992
unmount()
9093
})
9194

95+
it(`should not emit ${MonitorEvent.Monitor} event when paused`, () => {
96+
const monitorSelectorMock = jest.fn().mockReturnValue({
97+
isRunning: true,
98+
isPaused: true
99+
})
100+
monitorSelector.mockImplementation(monitorSelectorMock)
101+
102+
const { unmount } = render(<MonitorConfig />)
103+
const mockedMonitorEvent = jest.fn()
104+
105+
socket.socketClient.on(MonitorEvent.Monitor, mockedMonitorEvent)
106+
socket.socketClient.emit(SocketEvent.Connect)
107+
108+
expect(mockedMonitorEvent).not.toBeCalled()
109+
110+
unmount()
111+
})
112+
92113
it('monitor should catch Exception', () => {
93114
const { unmount } = render(<MonitorConfig />)
94115

redisinsight/ui/src/components/monitor-config/MonitorConfig.tsx

Lines changed: 73 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { useEffect } from 'react'
1+
import { useEffect, useRef } from 'react'
22
import { useDispatch, useSelector } from 'react-redux'
33
import { debounce } from 'lodash'
4-
import { io } from 'socket.io-client'
4+
import { io, Socket } from 'socket.io-client'
55
import { v4 as uuidv4 } from 'uuid'
66

77
import {
@@ -16,7 +16,7 @@ import {
1616
setLogFileId,
1717
pauseMonitor, lockResume
1818
} from 'uiSrc/slices/cli/monitor'
19-
import { getBaseApiUrl } from 'uiSrc/utils'
19+
import { getBaseApiUrl, Nullable } from 'uiSrc/utils'
2020
import { MonitorErrorMessages, MonitorEvent, SocketErrors, SocketEvent } from 'uiSrc/constants'
2121
import { IMonitorDataPayload } from 'uiSrc/slices/interfaces'
2222
import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'
@@ -26,12 +26,18 @@ import { IMonitorData } from 'apiSrc/modules/profiler/interfaces/monitor-data.in
2626
import ApiStatusCode from '../../constants/apiStatusCode'
2727

2828
interface IProps {
29-
retryDelay?: number;
29+
retryDelay?: number
3030
}
3131
const MonitorConfig = ({ retryDelay = 15000 } : IProps) => {
3232
const { id: instanceId = '' } = useSelector(connectedInstanceSelector)
3333
const { socket, isRunning, isPaused, isSaveToFile, isMinimizedMonitor, isShowMonitor } = useSelector(monitorSelector)
3434

35+
const socketRef = useRef<Nullable<Socket>>(null)
36+
const logFileIdRef = useRef<string>()
37+
const timestampRef = useRef<number>()
38+
const retryTimerRef = useRef<NodeJS.Timer>()
39+
const payloadsRef = useRef<IMonitorDataPayload[]>([])
40+
3541
const dispatch = useDispatch()
3642

3743
const setNewItems = debounce((items, onSuccess?) => {
@@ -52,83 +58,80 @@ const MonitorConfig = ({ retryDelay = 15000 } : IProps) => {
5258
if (!isRunning || !instanceId || socket?.connected) {
5359
return
5460
}
55-
const logFileId = `_redis_${uuidv4()}`
56-
const timestamp = Date.now()
57-
let retryTimer: NodeJS.Timer
61+
62+
logFileIdRef.current = `_redis_${uuidv4()}`
63+
timestampRef.current = Date.now()
5864

5965
// Create SocketIO connection to instance by instanceId
60-
const newSocket = io(`${getBaseApiUrl()}/monitor`, {
66+
socketRef.current = io(`${getBaseApiUrl()}/monitor`, {
6167
forceNew: true,
6268
query: { instanceId },
6369
extraHeaders: { [CustomHeaders.WindowId]: window.windowId || '' },
6470
rejectUnauthorized: false,
6571
})
66-
dispatch(setSocket(newSocket))
67-
let payloads: IMonitorDataPayload[] = []
68-
69-
const handleMonitorEvents = () => {
70-
dispatch(setMonitorLoadingPause(false))
71-
newSocket.on(MonitorEvent.MonitorData, (payload: IMonitorData[]) => {
72-
payloads = payloads.concat(payload)
73-
74-
// set batch of payloads and then clear batch
75-
setNewItems(payloads, () => {
76-
payloads.length = 0
77-
// reset all timings after items were changed
78-
setNewItems.cancel()
79-
})
80-
})
81-
}
72+
dispatch(setSocket(socketRef.current))
8273

8374
const handleDisconnect = () => {
84-
newSocket.removeAllListeners()
75+
socketRef.current?.removeAllListeners()
8576
dispatch(pauseMonitor())
8677
dispatch(stopMonitor())
8778
dispatch(lockResume())
8879
}
8980

90-
newSocket.on(SocketEvent.Connect, () => {
91-
// Trigger Monitor event
92-
clearTimeout(retryTimer)
93-
94-
dispatch(setLogFileId(logFileId))
95-
dispatch(setStartTimestamp(timestamp))
96-
newSocket.emit(
97-
MonitorEvent.Monitor,
98-
{ logFileId: isSaveToFile ? logFileId : null },
99-
handleMonitorEvents
100-
)
101-
})
102-
10381
// Catch exceptions
104-
newSocket.on(MonitorEvent.Exception, (payload) => {
82+
socketRef.current?.on(MonitorEvent.Exception, (payload) => {
10583
if (payload.status === ApiStatusCode.Forbidden) {
10684
handleDisconnect()
10785
dispatch(setError(MonitorErrorMessages.NoPerm))
10886
dispatch(resetMonitorItems())
10987
return
11088
}
11189

112-
payloads.push({ isError: true, time: `${Date.now()}`, ...payload })
113-
setNewItems(payloads, () => { payloads.length = 0 })
90+
payloadsRef.current.push({ isError: true, time: `${Date.now()}`, ...payload })
91+
setNewItems(payloadsRef.current, () => { payloads.length = 0 })
11492
dispatch(pauseMonitor())
11593
})
11694

11795
// Catch disconnect
118-
newSocket.on(SocketEvent.Disconnect, () => {
96+
socketRef.current?.on(SocketEvent.Disconnect, () => {
11997
if (retryDelay) {
120-
retryTimer = setTimeout(handleDisconnect, retryDelay)
98+
retryTimerRef.current = setTimeout(handleDisconnect, retryDelay)
12199
} else {
122100
handleDisconnect()
123101
}
124102
})
125103

126104
// Catch connect error
127-
newSocket.on(SocketEvent.ConnectionError, (error) => {
128-
payloads.push({ isError: true, time: `${Date.now()}`, message: getErrorMessage(error) })
129-
setNewItems(payloads, () => { payloads.length = 0 })
105+
socketRef.current?.on(SocketEvent.ConnectionError, (error) => {
106+
payloadsRef.current.push({ isError: true, time: `${Date.now()}`, message: getErrorMessage(error) })
107+
setNewItems(payloadsRef.current, () => { payloadsRef.current.length = 0 })
108+
})
109+
}, [instanceId, isRunning, isPaused])
110+
111+
useEffect(() => {
112+
if (!isRunning) {
113+
return
114+
}
115+
116+
socketRef.current?.removeAllListeners(SocketEvent.Connect)
117+
socketRef.current?.on(SocketEvent.Connect, () => {
118+
// Trigger Monitor event
119+
clearTimeout(retryTimerRef.current!)
120+
dispatch(setLogFileId(logFileIdRef.current))
121+
dispatch(setStartTimestamp(timestampRef.current))
122+
if (!isPaused) {
123+
subscribeMonitorEvents()
124+
}
130125
})
131-
}, [instanceId, isRunning, isSaveToFile])
126+
}, [isRunning, isPaused])
127+
128+
useEffect(() => {
129+
if (!isRunning || isPaused || !socketRef.current?.connected) {
130+
return
131+
}
132+
133+
subscribeMonitorEvents()
134+
}, [isRunning, isPaused])
132135

133136
useEffect(() => {
134137
if (!isRunning) return
@@ -150,6 +153,29 @@ const MonitorConfig = ({ retryDelay = 15000 } : IProps) => {
150153
}
151154
}, [socket, isRunning, isShowMonitor, isMinimizedMonitor])
152155

156+
const subscribeMonitorEvents = () => {
157+
socketRef.current?.removeAllListeners(MonitorEvent.MonitorData)
158+
socketRef.current?.emit(
159+
MonitorEvent.Monitor,
160+
{ logFileId: isSaveToFile ? logFileIdRef.current : null },
161+
handleMonitorEvents
162+
)
163+
}
164+
165+
const handleMonitorEvents = () => {
166+
dispatch(setMonitorLoadingPause(false))
167+
socketRef.current?.on(MonitorEvent.MonitorData, (payload: IMonitorData[]) => {
168+
payloadsRef.current = payloadsRef.current.concat(payload)
169+
170+
// set batch of payloads and then clear batch
171+
setNewItems(payloadsRef.current, () => {
172+
payloadsRef.current.length = 0
173+
// reset all timings after items were changed
174+
setNewItems.cancel()
175+
})
176+
})
177+
}
178+
153179
return null
154180
}
155181

redisinsight/ui/src/constants/bulkActions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export enum BulkActionsStatus {
2020
Completed = 'completed',
2121
Failed = 'failed',
2222
Aborted = 'aborted',
23+
Disconnected = 'disconnected'
2324
}
2425

2526
export const MAX_BULK_ACTION_ERRORS_LENGTH = 500

redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsInfo/BulkActionsInfo.spec.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react'
22
import { mock } from 'ts-mockito'
3-
import { KeyTypes } from 'uiSrc/constants'
3+
import { BulkActionsStatus, KeyTypes } from 'uiSrc/constants'
44
import { render, screen } from 'uiSrc/utils/test-utils'
55

66
import BulkActionsInfo, { Props } from './BulkActionsInfo'
@@ -25,4 +25,10 @@ describe('BulkActionsInfo', () => {
2525

2626
expect(screen.queryByTestId('bulk-actions-info-filter')).not.toBeInTheDocument()
2727
})
28+
29+
it('should show connection lost when status is disconnect', () => {
30+
render(<BulkActionsInfo {...mockedProps} filter={null} status={BulkActionsStatus.Disconnected} />)
31+
32+
expect(screen.getByTestId('bulk-status-disconnected')).toHaveTextContent('Connection Lost')
33+
})
2834
})

redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsInfo/BulkActionsInfo.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { getApproximatePercentage, Maybe, Nullable } from 'uiSrc/utils'
77
import Divider from 'uiSrc/components/divider/Divider'
88
import { BulkActionsStatus, KeyTypes } from 'uiSrc/constants'
99
import GroupBadge from 'uiSrc/components/group-badge/GroupBadge'
10+
import { isProcessedBulkAction } from 'uiSrc/pages/browser/components/bulk-actions/utils'
1011
import styles from './styles.module.scss'
1112

1213
export interface Props {
@@ -46,7 +47,7 @@ const BulkActionsInfo = (props: Props) => {
4647
</div>
4748
)}
4849
</EuiText>
49-
{!isUndefined(status) && status !== BulkActionsStatus.Completed && status !== BulkActionsStatus.Aborted && (
50+
{!isUndefined(status) && !isProcessedBulkAction(status) && (
5051
<EuiText color="subdued" className={styles.progress} data-testid="bulk-status-progress">
5152
In progress:
5253
<span>{` ${getApproximatePercentage(total, scanned)}`}</span>
@@ -62,6 +63,11 @@ const BulkActionsInfo = (props: Props) => {
6263
Action completed
6364
</EuiText>
6465
)}
66+
{status === BulkActionsStatus.Disconnected && (
67+
<EuiText color="danger" className={styles.progress} data-testid="bulk-status-disconnected">
68+
Connection Lost: {getApproximatePercentage(total, scanned)}
69+
</EuiText>
70+
)}
6571
</div>
6672
<Divider colorVariable="separatorColor" className={styles.divider} />
6773
{loading && (

redisinsight/ui/src/pages/browser/components/bulk-actions/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export const isProcessedBulkAction = (status?: BulkActionsStatus) =>
99
status === BulkActionsStatus.Completed
1010
|| status === BulkActionsStatus.Aborted
1111
|| status === BulkActionsStatus.Failed
12+
|| status === BulkActionsStatus.Disconnected

0 commit comments

Comments
 (0)