Skip to content

Commit 82d3937

Browse files
committed
#RI-4840 - [Regression] Profiler logs download failure
1 parent 26f8ad5 commit 82d3937

File tree

6 files changed

+131
-36
lines changed

6 files changed

+131
-36
lines changed

redisinsight/ui/src/components/monitor/MonitorLog/MonitorLog.spec.tsx

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { cloneDeep } from 'lodash'
22
import React from 'react'
3-
import { saveAs } from 'file-saver'
43
import { monitorSelector, resetProfiler, stopMonitor } from 'uiSrc/slices/cli/monitor'
54
import { act, cleanup, fireEvent, mockedStore, render, screen } from 'uiSrc/utils/test-utils'
5+
import { sendCliCommand } from 'uiSrc/slices/cli/cli-output'
66
import MonitorLog from './MonitorLog'
77

88
let store: typeof mockedStore
@@ -28,20 +28,10 @@ jest.mock('uiSrc/slices/cli/monitor', () => ({
2828
}),
2929
}))
3030

31-
global.Blob = function (content, options) { return ({ content, options }) }
32-
global.fetch = jest.fn(() =>
33-
Promise.resolve({
34-
text: () => Promise.resolve('123'),
35-
headers: {
36-
get: () => '123"filename.txt"oeu',
37-
}
38-
}))
39-
4031
beforeEach(() => {
4132
cleanup()
4233
store = cloneDeep(mockedStore)
4334
store.clearActions()
44-
fetch.mockClear()
4535
})
4636

4737
describe('MonitorLog', () => {
@@ -61,8 +51,7 @@ describe('MonitorLog', () => {
6151
expect(store.getActions()).toEqual(expectedActions)
6252
})
6353

64-
it('should call download a file', async () => {
65-
const saveAsMock = jest.fn()
54+
it('should call fetchMonitorLog after click on Download', async () => {
6655
const monitorSelectorMock = jest.fn().mockReturnValue({
6756
isSaveToFile: true,
6857
logFileId: 'logFileId',
@@ -74,19 +63,15 @@ describe('MonitorLog', () => {
7463
}
7564
});
7665

77-
(monitorSelector as jest.Mock).mockImplementation(monitorSelectorMock);
78-
(saveAs as jest.Mock).mockImplementation(() => saveAsMock)
66+
(monitorSelector as jest.Mock).mockImplementation(monitorSelectorMock)
7967

8068
render(<MonitorLog />)
8169

8270
await act(() => {
8371
fireEvent.click(screen.getByTestId('download-log-btn'))
8472
})
8573

86-
expect(saveAs).toBeCalledWith(
87-
{ content: ['123'], options: { type: 'text/plain;charset=utf-8' } },
88-
'filename.txt',
89-
)
90-
saveAs.mockRestore()
74+
const expectedActions = [sendCliCommand()]
75+
expect(store.getActions().slice(0, expectedActions.length)).toEqual(expectedActions)
9176
})
9277
})

redisinsight/ui/src/components/monitor/MonitorLog/MonitorLog.tsx

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@ import { format, formatDuration, intervalToDuration } from 'date-fns'
33
import React from 'react'
44
import { useDispatch, useSelector } from 'react-redux'
55
import AutoSizer from 'react-virtualized-auto-sizer'
6-
import { saveAs } from 'file-saver'
76

8-
import { ApiEndpoints } from 'uiSrc/constants'
97
import { monitorSelector, resetProfiler, stopMonitor } from 'uiSrc/slices/cli/monitor'
10-
import { cutDurationText, getBaseApiUrl } from 'uiSrc/utils'
11-
import { CustomHeaders } from 'uiSrc/constants/api'
8+
import { cutDurationText } from 'uiSrc/utils'
9+
import { downloadFile } from 'uiSrc/utils/dom/downloadFile'
10+
import { fetchMonitorLog } from 'uiSrc/slices/cli/cli-output'
1211

1312
import styles from './styles.module.scss'
1413

@@ -47,18 +46,8 @@ const MonitorLog = () => {
4746

4847
const handleDownloadLog = async (e: React.MouseEvent<HTMLAnchorElement>) => {
4948
e.preventDefault()
50-
const baseApiUrl = getBaseApiUrl()
51-
const linkToDownload = `${baseApiUrl}/api/${ApiEndpoints.PROFILER_LOGS}/${logFileId}`
5249

53-
const response = await fetch(
54-
linkToDownload,
55-
{ headers: { [CustomHeaders.WindowId]: window.windowId || '' } },
56-
)
57-
58-
const contentDisposition = response.headers.get('Content-Disposition') || ''
59-
const file = new Blob([await response.text()], { type: 'text/plain;charset=utf-8' })
60-
61-
saveAs(file, contentDisposition.split('"')?.[1])
50+
dispatch(fetchMonitorLog(logFileId || '', downloadFile))
6251
}
6352

6453
return (

redisinsight/ui/src/slices/cli/cli-output.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createSlice } from '@reduxjs/toolkit'
22

3+
import { AxiosError, AxiosResponseHeaders } from 'axios'
34
import { CliOutputFormatterType, cliTexts, ConnectionSuccessOutputText, SelectCommand } from 'uiSrc/constants/cliOutput'
45
import { apiService, localStorageService } from 'uiSrc/services'
56
import { ApiEndpoints, BrowserStorageItem, CommandMonitor, } from 'uiSrc/constants'
@@ -16,6 +17,7 @@ import { SendClusterCommandDto, SendClusterCommandResponse, SendCommandResponse,
1617

1718
import { AppDispatch, RootState } from '../store'
1819
import { CommandExecutionStatus, StateCliOutput } from '../interfaces/cli'
20+
import { addErrorNotification } from '../app/notifications'
1921

2022
export const initialState: StateCliOutput = {
2123
data: [],
@@ -278,3 +280,29 @@ function handleRecreateClient(dispatch: AppDispatch, stateInit: () => RootState,
278280
))
279281
}
280282
}
283+
284+
// Asynchronous thunk action
285+
export function fetchMonitorLog(
286+
logFileId: string = '',
287+
onSuccessAction?: (data: string, headers: AxiosResponseHeaders) => void,
288+
) {
289+
return async (dispatch: AppDispatch) => {
290+
dispatch(sendCliCommand())
291+
292+
try {
293+
const { data, status, headers } = await apiService.get<string>(
294+
`${ApiEndpoints.PROFILER_LOGS}/${logFileId}`
295+
)
296+
297+
if (isStatusSuccessful(status)) {
298+
dispatch(sendCliCommandSuccess())
299+
onSuccessAction?.(data, headers)
300+
}
301+
} catch (err) {
302+
const error = err as AxiosError
303+
const errorMessage = getApiErrorMessage(error)
304+
dispatch(addErrorNotification(error))
305+
dispatch(sendCliCommandFailure(errorMessage))
306+
}
307+
}
308+
}

redisinsight/ui/src/slices/tests/cli/cli-output.spec.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import { cloneDeep, first } from 'lodash'
22

3+
import { AxiosError } from 'axios'
34
import { AppDispatch, RootState } from 'uiSrc/slices/store'
45
import { cleanup, clearStoreActions, initialStateDefault, mockedStore, mockStore, } from 'uiSrc/utils/test-utils'
56
import { ClusterNodeRole, CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli'
67
import { apiService } from 'uiSrc/services'
78
import { cliTexts } from 'uiSrc/constants/cliOutput'
89
import { cliParseTextResponseWithOffset, cliParseTextResponseWithRedirect } from 'uiSrc/utils/cliHelper'
910
import ApiErrors from 'uiSrc/constants/apiErrors'
10-
import { processCliClient, updateCliClientAction } from 'uiSrc/slices/cli/cli-settings'
11+
import { processCliClient } from 'uiSrc/slices/cli/cli-settings'
12+
import { addErrorNotification } from 'uiSrc/slices/app/notifications'
1113
import { SendClusterCommandDto, SendClusterCommandResponse } from 'apiSrc/modules/cli/dto/cli.dto'
1214
import reducer, {
1315
concatToOutput,
16+
fetchMonitorLog,
1417
initialState,
1518
outputSelector,
1619
processUnsupportedCommand,
@@ -488,4 +491,54 @@ describe('cliOutput slice', () => {
488491
})
489492
})
490493
})
494+
495+
describe('fetchMonitorLog', () => {
496+
it('call both sendCliCommand and sendCliCommandSuccess when fetch is successed', async () => {
497+
// Arrange
498+
const fileIdMock = 'fileId'
499+
const onSuccessActionMock = jest.fn()
500+
const data = 'test'
501+
const responsePayload = { data, status: 200 }
502+
503+
apiService.get = jest.fn().mockResolvedValue(responsePayload)
504+
505+
// Act
506+
await store.dispatch<any>(fetchMonitorLog(fileIdMock, onSuccessActionMock))
507+
508+
// Assert
509+
const expectedActions = [
510+
sendCliCommand(),
511+
sendCliCommandSuccess(),
512+
]
513+
expect(store.getActions()).toEqual(expectedActions)
514+
expect(onSuccessActionMock).toBeCalled()
515+
})
516+
517+
it('call both sendCliCommand and sendCliCommandFailure when fetch is fail', async () => {
518+
// Arrange
519+
const fileIdMock = 'fileId'
520+
const onSuccessActionMock = jest.fn()
521+
const errorMessage = 'Could not connect to aoeu:123, please check the connection details.'
522+
const responsePayload = {
523+
response: {
524+
status: 500,
525+
data: { message: errorMessage },
526+
},
527+
}
528+
529+
apiService.get = jest.fn().mockRejectedValueOnce(responsePayload)
530+
531+
// Act
532+
await store.dispatch<any>(fetchMonitorLog(fileIdMock, onSuccessActionMock))
533+
534+
// Assert
535+
const expectedActions = [
536+
sendCliCommand(),
537+
addErrorNotification(responsePayload as AxiosError),
538+
sendCliCommandFailure(responsePayload.response.data.message),
539+
]
540+
expect(store.getActions()).toEqual(expectedActions)
541+
expect(onSuccessActionMock).not.toBeCalled()
542+
})
543+
})
491544
})
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { saveAs } from 'file-saver'
2+
import { AxiosResponseHeaders } from 'axios'
3+
4+
export const DEFAULT_FILE_NAME = 'RedisInsight'
5+
6+
export const downloadFile = (data: string = '', headers: AxiosResponseHeaders) => {
7+
const contentDisposition = headers?.['content-disposition'] || ''
8+
const file = new Blob([data], { type: 'text/plain;charset=utf-8' })
9+
const fileName = contentDisposition.split('"')?.[1] || DEFAULT_FILE_NAME
10+
11+
saveAs(file, fileName)
12+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { saveAs } from 'file-saver'
2+
import { DEFAULT_FILE_NAME, downloadFile } from 'uiSrc/utils/dom/downloadFile'
3+
4+
jest.mock('file-saver', () => ({
5+
...jest.requireActual('file-saver'),
6+
saveAs: jest.fn(),
7+
}))
8+
9+
const getDownloadFileTests: any[] = [
10+
['5123123123', { 'content-disposition': '123"123"123' }, '123'],
11+
['test\ntest123', { 'content-disposition': '123"filename.txt"123' }, 'filename.txt'],
12+
['5123 uoeu aoue ao123123', { 'content-disposition': '123"1uaoeutaoeu"123' }, '1uaoeutaoeu'],
13+
[null, { 'content-disposition': '123"123"123' }, '123'],
14+
['5123 3', {}, DEFAULT_FILE_NAME],
15+
]
16+
17+
describe('downloadFile', () => {
18+
it.each(getDownloadFileTests)('saveAs should be called with: %s (data), %s (headers), ',
19+
(data: string, headers, fileName: string) => {
20+
const saveAsMock = jest.fn();
21+
(saveAs as jest.Mock).mockImplementation(() => saveAsMock)
22+
23+
downloadFile(data, headers)
24+
expect(saveAs).toBeCalledWith(
25+
new Blob([data], { type: 'text/plain;charset=utf-8' }), fileName,
26+
)
27+
})
28+
})

0 commit comments

Comments
 (0)