Skip to content

Commit ceb5e20

Browse files
Merge pull request #3036 from RedisInsight/fe/feature/RI-5394-redis-upload
#RI-5394 - add executable param for redis queries, add download butto…
2 parents 8dfa250 + 39618c4 commit ceb5e20

File tree

11 files changed

+199
-69
lines changed

11 files changed

+199
-69
lines changed

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>

redisinsight/ui/src/components/database-side-panels/panels/enablement-area/EnablementArea/components/RedisUploadButton/styles.module.scss

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.wrapper {
22
width: 100%;
3+
max-width: 480px;
34

45
:global(.euiPopover) {
56
width: 100%;
@@ -16,13 +17,12 @@
1617
&:global(.euiButton.euiButton-isDisabled) {
1718
color: var(--buttonSecondaryDisabledTextColor) !important;
1819
}
19-
}
20-
}
2120

22-
.containerPopover {
23-
padding: 18px;
24-
width: 360px;
25-
height: 188px;
21+
:global(.euiIcon) {
22+
width: 14px;
23+
height: 14px;
24+
}
25+
}
2626
}
2727

2828
.popover {
@@ -47,25 +47,34 @@
4747

4848
.popoverIcon {
4949
position: absolute;
50+
top: 14px;
51+
left: 14px;
52+
5053
color: var(--euiColorWarningLight) !important;
51-
width: 24px !important;
52-
height: 24px !important;
54+
width: 18px !important;
55+
height: 18px !important;
5356
}
5457

5558
.popoverItem {
5659
font-size: 13px !important;
5760
line-height: 18px !important;
58-
padding-left: 34px;
61+
padding-left: 30px;
62+
}
63+
64+
.link {
65+
color: var(--externalLinkColor) !important;
66+
}
67+
68+
.popoverActions {
69+
display: flex;
70+
align-items: center;
71+
justify-content: space-between;
72+
73+
padding-left: 30px;
5974
}
6075

6176
.popoverItemTitle {
6277
color: var(--htmlColor) !important;
6378
font-size: 14px !important;
6479
line-height: 24px !important;
6580
}
66-
67-
.uploadApproveBtn {
68-
position: absolute;
69-
right: 18px;
70-
bottom: 18px;
71-
}

redisinsight/ui/src/constants/workbench.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export interface CodeButtonParams {
3838
results?: keyof typeof CodeButtonResults
3939
mode?: keyof typeof CodeButtonRunQueryMode
4040
run_confirmation?: keyof typeof BooleanParams
41+
executable?: keyof typeof BooleanParams
4142
}
4243

4344
export enum ExecuteButtonMode {

redisinsight/ui/src/services/resourcesService.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,6 @@ export const getPathToResource = (url: string = ''): string => (IS_ABSOLUTE_PATH
2020
? url
2121
: new URL(url, resourcesService.defaults.baseURL).toString())
2222

23+
export const checkResourse = async (url: string = '') => resourcesService.head(url)
24+
2325
export default resourcesService

0 commit comments

Comments
 (0)