Skip to content

Commit 65b8861

Browse files
Merge pull request #3033 from RedisInsight/feature/RI-5394-redis-upload
Feature/ri 5394 redis upload
2 parents ad59f3e + ceb5e20 commit 65b8861

File tree

12 files changed

+209
-69
lines changed

12 files changed

+209
-69
lines changed

redisinsight/api/src/modules/statics-management/statics-management.module.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
22
import { ServeStaticModule } from '@nestjs/serve-static';
33
import { join } from 'path';
44
import config, { Config } from 'src/utils/config';
5+
import { Response } from 'express';
56
import { AutoUpdatedStaticsProvider } from './providers/auto-updated-statics.provider';
67

78
const SERVER_CONFIG = config.get('server') as Config['server'];
@@ -10,20 +11,29 @@ const TUTORIALS_CONFIG = config.get('tutorials') as Config['tutorials'];
1011

1112
const CONTENT_CONFIG = config.get('content');
1213

14+
const downloadableStaticFiles = (res: Response) => {
15+
if (res.req?.query?.download === 'true') {
16+
res.setHeader('Content-Type', 'application/octet-stream');
17+
res.setHeader('Content-Disposition', 'attachment;');
18+
}
19+
};
20+
1321
@Module({
1422
imports: [
1523
ServeStaticModule.forRoot({
1624
serveRoot: SERVER_CONFIG.tutorialsUri,
1725
rootPath: join(PATH_CONFIG.tutorials),
1826
serveStaticOptions: {
1927
fallthrough: false,
28+
setHeaders: downloadableStaticFiles,
2029
},
2130
}),
2231
ServeStaticModule.forRoot({
2332
serveRoot: SERVER_CONFIG.customTutorialsUri,
2433
rootPath: join(PATH_CONFIG.customTutorials),
2534
serveStaticOptions: {
2635
fallthrough: false,
36+
setHeaders: downloadableStaticFiles,
2737
},
2838
}),
2939
ServeStaticModule.forRoot({

redisinsight/ui/src/components/database-side-panels/panels/enablement-area/EnablementArea/components/CodeButtonBlock/CodeButtonBlock.spec.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,22 @@ describe('CodeButtonBlock', () => {
7373
expect(onApply).toBeCalledWith({ pipeline: '10' }, expect.any(Function))
7474
})
7575

76+
it('should not render run button with executable=false param', () => {
77+
const onApply = jest.fn()
78+
79+
render(
80+
<CodeButtonBlock
81+
{...instance(mockedProps)}
82+
label={label}
83+
onApply={onApply}
84+
params={{ executable: 'false' }}
85+
content={simpleContent}
86+
/>
87+
)
88+
89+
expect(screen.queryByTestId(`run-btn-${label}`)).not.toBeInTheDocument()
90+
})
91+
7692
it('should go to home page after click on change db', async () => {
7793
const pushMock = jest.fn()
7894
reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })

redisinsight/ui/src/components/database-side-panels/panels/enablement-area/EnablementArea/components/CodeButtonBlock/CodeButtonBlock.tsx

Lines changed: 40 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const CodeButtonBlock = (props: Props) => {
5656
ConfigDBStorageItem.notShowConfirmationRunTutorial
5757
)
5858
const isButtonHasConfirmation = params?.run_confirmation === BooleanParams.true
59+
const isRunButtonHidden = params?.executable === BooleanParams.false
5960
const [notLoadedModule] = getUnsupportedModulesFromQuery(modules, content)
6061

6162
useEffect(() => {
@@ -134,44 +135,46 @@ const CodeButtonBlock = (props: Props) => {
134135
>
135136
Copy
136137
</EuiButton>
137-
<EuiPopover
138-
ownFocus
139-
initialFocus={false}
140-
className={styles.popoverAnchor}
141-
panelClassName={cx('euiToolTip', 'popoverLikeTooltip', styles.popover)}
142-
anchorClassName={styles.popoverAnchor}
143-
anchorPosition="upLeft"
144-
isOpen={isPopoverOpen}
145-
panelPaddingSize="m"
146-
closePopover={handleClosePopover}
147-
focusTrapProps={{
148-
scrollLock: true
149-
}}
150-
button={(
151-
<EuiToolTip
152-
anchorClassName={styles.popoverAnchor}
153-
content={isPopoverOpen ? undefined : 'Open Workbench in the left menu to see the command results.'}
154-
data-testid="run-btn-open-workbench-tooltip"
155-
>
156-
<EuiButton
157-
onClick={handleRunClicked}
158-
iconType={isRunned ? 'check' : 'play'}
159-
iconSide="right"
160-
color="success"
161-
size="s"
162-
disabled={isLoading || isRunned}
163-
isLoading={isLoading}
164-
className={cx(styles.actionBtn, styles.runBtn)}
165-
{...rest}
166-
data-testid={`run-btn-${label}`}
138+
{!isRunButtonHidden && (
139+
<EuiPopover
140+
ownFocus
141+
initialFocus={false}
142+
className={styles.popoverAnchor}
143+
panelClassName={cx('euiToolTip', 'popoverLikeTooltip', styles.popover)}
144+
anchorClassName={styles.popoverAnchor}
145+
anchorPosition="upLeft"
146+
isOpen={isPopoverOpen}
147+
panelPaddingSize="m"
148+
closePopover={handleClosePopover}
149+
focusTrapProps={{
150+
scrollLock: true
151+
}}
152+
button={(
153+
<EuiToolTip
154+
anchorClassName={styles.popoverAnchor}
155+
content={isPopoverOpen ? undefined : 'Open Workbench in the left menu to see the command results.'}
156+
data-testid="run-btn-open-workbench-tooltip"
167157
>
168-
Run
169-
</EuiButton>
170-
</EuiToolTip>
171-
)}
172-
>
173-
{getPopoverMessage()}
174-
</EuiPopover>
158+
<EuiButton
159+
onClick={handleRunClicked}
160+
iconType={isRunned ? 'check' : 'play'}
161+
iconSide="right"
162+
color="success"
163+
size="s"
164+
disabled={isLoading || isRunned}
165+
isLoading={isLoading}
166+
className={cx(styles.actionBtn, styles.runBtn)}
167+
{...rest}
168+
data-testid={`run-btn-${label}`}
169+
>
170+
Run
171+
</EuiButton>
172+
</EuiToolTip>
173+
)}
174+
>
175+
{getPopoverMessage()}
176+
</EuiPopover>
177+
)}
175178
</EuiFlexItem>
176179
</EuiFlexGroup>
177180
<div className={styles.content} data-testid="code-button-block-content">

redisinsight/ui/src/components/database-side-panels/panels/enablement-area/EnablementArea/components/RedisUploadButton/RedisUploadButton.spec.tsx

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import React from 'react'
22
import { cloneDeep } from 'lodash'
33
import reactRouterDom from 'react-router-dom'
4-
import { cleanup, fireEvent, mockedStore, render, screen } from 'uiSrc/utils/test-utils'
4+
import { AxiosError } from 'axios'
5+
import { cleanup, fireEvent, mockedStore, render, screen, act } from 'uiSrc/utils/test-utils'
56
import { customTutorialsBulkUploadSelector, uploadDataBulk } from 'uiSrc/slices/workbench/wb-custom-tutorials'
67

78
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
9+
import { checkResourse } from 'uiSrc/services/resourcesService'
10+
import { addErrorNotification } from 'uiSrc/slices/app/notifications'
811
import RedisUploadButton, { Props } from './RedisUploadButton'
912

1013
jest.mock('uiSrc/slices/workbench/wb-custom-tutorials', () => ({
@@ -14,6 +17,11 @@ jest.mock('uiSrc/slices/workbench/wb-custom-tutorials', () => ({
1417
}),
1518
}))
1619

20+
jest.mock('uiSrc/services/resourcesService', () => ({
21+
...jest.requireActual('uiSrc/services/resourcesService'),
22+
checkResourse: jest.fn(),
23+
}))
24+
1725
jest.mock('uiSrc/telemetry', () => ({
1826
...jest.requireActual('uiSrc/telemetry'),
1927
sendEventTelemetry: jest.fn(),
@@ -31,6 +39,10 @@ const props: Props = {
3139
path: '/text'
3240
}
3341

42+
const error = {
43+
response: { data: { message: 'File not found. Check if this file exists and try again.' } }
44+
} as AxiosError<any>
45+
3446
describe('RedisUploadButton', () => {
3547
beforeEach(() => {
3648
reactRouterDom.useParams = jest.fn().mockReturnValue({ instanceId: 'instanceId' })
@@ -72,7 +84,22 @@ describe('RedisUploadButton', () => {
7284
expect(screen.getByTestId('database-not-opened-popover')).toBeInTheDocument()
7385
})
7486

75-
it('should call proper telemetry events', () => {
87+
it('should show error when file is not exists', async () => {
88+
const checkResourseMock = jest.fn().mockRejectedValue('');
89+
(checkResourse as jest.Mock).mockImplementation(checkResourseMock)
90+
91+
render(<RedisUploadButton {...props} />)
92+
93+
fireEvent.click(screen.getByTestId('upload-data-bulk-btn'))
94+
await act(() => {
95+
fireEvent.click(screen.getByTestId('download-redis-upload-file'))
96+
})
97+
98+
expect(checkResourseMock).toBeCalledWith('http://localhost:5001/text')
99+
expect(store.getActions()).toEqual([addErrorNotification(error)])
100+
})
101+
102+
it('should call proper telemetry events', async () => {
76103
const sendEventTelemetryMock = jest.fn();
77104
(sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock)
78105
render(<RedisUploadButton {...props} />)
@@ -88,6 +115,19 @@ describe('RedisUploadButton', () => {
88115

89116
(sendEventTelemetry as jest.Mock).mockRestore()
90117

118+
await act(() => {
119+
fireEvent.click(screen.getByTestId('download-redis-upload-file'))
120+
})
121+
122+
expect(sendEventTelemetry).toBeCalledWith({
123+
event: TelemetryEvent.EXPLORE_PANEL_DOWNLOAD_BULK_FILE_CLICKED,
124+
eventData: {
125+
databaseId: 'instanceId'
126+
}
127+
});
128+
129+
(sendEventTelemetry as jest.Mock).mockRestore()
130+
91131
fireEvent.click(screen.getByTestId('upload-data-bulk-apply-btn'))
92132

93133
expect(sendEventTelemetry).toBeCalledWith({

redisinsight/ui/src/components/database-side-panels/panels/enablement-area/EnablementArea/components/RedisUploadButton/RedisUploadButton.tsx

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
import { EuiButton, EuiIcon, EuiPopover, EuiSpacer, EuiText } from '@elastic/eui'
1+
import { EuiButton, EuiIcon, EuiLink, EuiPopover, EuiSpacer, EuiText } from '@elastic/eui'
22
import { useDispatch, useSelector } from 'react-redux'
33
import React, { useEffect, useState } from 'react'
44
import cx from 'classnames'
55
import { useParams } from 'react-router-dom'
6+
import { AxiosError } from 'axios'
67
import { truncateText } from 'uiSrc/utils'
78
import { sendEventTelemetry, TELEMETRY_EMPTY_VALUE, TelemetryEvent } from 'uiSrc/telemetry'
89
import { customTutorialsBulkUploadSelector, uploadDataBulkAction } from 'uiSrc/slices/workbench/wb-custom-tutorials'
910

10-
import { ReactComponent as BulkDataUploadIcon } from 'uiSrc/assets/img/icons/data-upload-bulk.svg'
1111
import DatabaseNotOpened from 'uiSrc/components/messages/database-not-opened'
1212

13+
import { checkResourse, getPathToResource } from 'uiSrc/services/resourcesService'
14+
import { addErrorNotification } from 'uiSrc/slices/app/notifications'
1315
import styles from './styles.module.scss'
1416

1517
export interface Props {
@@ -26,6 +28,8 @@ const RedisUploadButton = ({ label, path }: Props) => {
2628
const dispatch = useDispatch()
2729
const { instanceId } = useParams<{ instanceId: string }>()
2830

31+
const urlToFile = getPathToResource(path)
32+
2933
useEffect(() => {
3034
setIsLoading(pathsInProgress.includes(path))
3135
}, [pathsInProgress])
@@ -54,20 +58,48 @@ const RedisUploadButton = ({ label, path }: Props) => {
5458
})
5559
}
5660

61+
const handleDownload = async (e: React.MouseEvent) => {
62+
e.preventDefault()
63+
64+
try {
65+
await checkResourse(urlToFile)
66+
67+
const downloadAnchor = document.createElement('a')
68+
downloadAnchor.setAttribute('href', `${urlToFile}?download=true`)
69+
downloadAnchor.setAttribute('download', label)
70+
downloadAnchor.click()
71+
} catch {
72+
const error = {
73+
response: { data: { message: 'File not found. Check if this file exists and try again.' } }
74+
} as AxiosError<any>
75+
dispatch(addErrorNotification(error))
76+
}
77+
78+
sendEventTelemetry({
79+
event: TelemetryEvent.EXPLORE_PANEL_DOWNLOAD_BULK_FILE_CLICKED,
80+
eventData: {
81+
databaseId: instanceId
82+
}
83+
})
84+
}
85+
5786
return (
5887
<div className={cx(styles.wrapper, 'mb-s mt-s')}>
5988
<EuiPopover
89+
ownFocus
90+
initialFocus={false}
6091
id="upload-data-bulk-btn"
6192
anchorPosition="downLeft"
6293
isOpen={isPopoverOpen}
6394
closePopover={() => setIsPopoverOpen(false)}
64-
panelClassName={instanceId ? styles.panelPopover : cx('euiToolTip', 'popoverLikeTooltip', styles.popover)}
95+
panelClassName={cx('euiToolTip', 'popoverLikeTooltip', styles.popover)}
6596
anchorClassName={styles.popoverAnchor}
6697
panelPaddingSize="none"
6798
button={(
6899
<EuiButton
69100
isLoading={isLoading}
70-
iconType={BulkDataUploadIcon}
101+
iconSide="right"
102+
iconType="indexRuntime"
71103
size="s"
72104
className={styles.button}
73105
onClick={openPopover}
@@ -93,16 +125,24 @@ const RedisUploadButton = ({ label, path }: Props) => {
93125
All commands from the file in your tutorial will be automatically executed against your database.
94126
Avoid executing them in production databases.
95127
</div>
96-
<EuiButton
97-
fill
98-
size="s"
99-
color="secondary"
100-
className={styles.uploadApproveBtn}
101-
onClick={uploadData}
102-
data-testid="upload-data-bulk-apply-btn"
103-
>
104-
Execute
105-
</EuiButton>
128+
<EuiSpacer size="m" />
129+
<div className={styles.popoverActions}>
130+
<EuiLink onClick={handleDownload} className={styles.link} data-testid="download-redis-upload-file">
131+
Download file
132+
</EuiLink>
133+
<EuiButton
134+
fill
135+
size="s"
136+
color="secondary"
137+
iconType="playFilled"
138+
iconSide="right"
139+
className={styles.uploadApproveBtn}
140+
onClick={uploadData}
141+
data-testid="upload-data-bulk-apply-btn"
142+
>
143+
Execute
144+
</EuiButton>
145+
</div>
106146
</EuiText>
107147
) : (<DatabaseNotOpened />)}
108148
</EuiPopover>

0 commit comments

Comments
 (0)