Skip to content

Commit bd801f9

Browse files
authored
Merge pull request #1741 from RedisInsight/fe/feature/RI-4061_upload_json
Fe/feature/ri 4061 upload json
2 parents b1f7a4a + d52987b commit bd801f9

File tree

11 files changed

+267
-17
lines changed

11 files changed

+267
-17
lines changed

redisinsight/ui/src/components/monaco-json/MonacoJson.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useContext, useEffect, useRef, useState } from 'react'
1+
import React, { useContext, useEffect, useRef } from 'react'
22
import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'
33
import MonacoEditor, { monaco } from 'react-monaco-editor'
44
import cx from 'classnames'
@@ -19,13 +19,12 @@ export interface Props {
1919
}
2020
const MonacoJson = (props: Props) => {
2121
const {
22-
value: valueProp,
22+
value,
2323
onChange,
2424
disabled,
2525
wrapperClassName,
2626
'data-testid': dataTestId
2727
} = props
28-
const [value, setValue] = useState<string>(valueProp)
2928
const monacoObjects = useRef<Nullable<IEditorMount>>(null)
3029

3130
const { theme } = useContext(ThemeContext)
@@ -34,11 +33,6 @@ const MonacoJson = (props: Props) => {
3433
monacoObjects.current?.editor.updateOptions({ readOnly: disabled })
3534
}, [disabled])
3635

37-
const handleChange = (val: string) => {
38-
setValue(val)
39-
onChange(val)
40-
}
41-
4236
const editorDidMount = (
4337
editor: monacoEditor.editor.IStandaloneCodeEditor,
4438
monaco: typeof monacoEditor,
@@ -83,7 +77,7 @@ const MonacoJson = (props: Props) => {
8377
language="json"
8478
theme={theme === Theme.Dark ? 'dark' : 'light'}
8579
value={value}
86-
onChange={handleChange}
80+
onChange={onChange}
8781
options={options}
8882
className="json-monaco-editor"
8983
editorDidMount={editorDidMount}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import React from 'react'
2+
import { instance, mock } from 'ts-mockito'
3+
import { render } from 'uiSrc/utils/test-utils'
4+
5+
import UploadFile, { Props } from './UploadFile'
6+
7+
const mockedProps = mock<Props>()
8+
9+
describe('UploadFile', () => {
10+
it('should render', () => {
11+
expect(
12+
render(<UploadFile {...instance(mockedProps)} />)
13+
).toBeTruthy()
14+
})
15+
})
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React from 'react'
2+
import { EuiButtonEmpty, EuiText } from '@elastic/eui'
3+
4+
import styles from './styles.module.scss'
5+
6+
export interface Props {
7+
onFileChange: ({ target: { files } }: { target: { files: FileList | null } }) => void
8+
onClick: () => void
9+
}
10+
11+
const UploadFile = ({ onFileChange, onClick }: Props) => (
12+
<EuiButtonEmpty
13+
iconType="folderOpen"
14+
className={styles.emptyBtn}
15+
>
16+
<label htmlFor="upload-input-file" className={styles.uploadBtn}>
17+
<EuiText className={styles.label}>Upload</EuiText>
18+
<input
19+
type="file"
20+
id="upload-input-file"
21+
data-testid="upload-input-file"
22+
accept="application/json, text/plain"
23+
onChange={onFileChange}
24+
onClick={onClick}
25+
className={styles.fileDrop}
26+
aria-label="Select file"
27+
/>
28+
</label>
29+
</EuiButtonEmpty>
30+
)
31+
32+
export default UploadFile
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import UploadFile from './UploadFile'
2+
3+
export default UploadFile
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.fileDrop {
2+
display: none;
3+
}
4+
5+
.uploadBtn {
6+
display: flex;
7+
cursor: pointer;
8+
}
9+
10+
.emptyBtn:global(.euiButtonEmpty) {
11+
height: 22px;
12+
margin-top: 7px;
13+
}
14+
15+
.emptyBtn:global(.euiButtonEmpty .euiButtonEmpty__content) {
16+
padding: 0 12px;
17+
}
18+
19+
:global(.euiButtonEmpty.euiButtonEmpty--primary).emptyBtn .label {
20+
color: var(--inputTextColor) !important;
21+
line-height: 16px !important;
22+
font-weight: 400 !important;
23+
font-size: 12px !important;
24+
}

redisinsight/ui/src/pages/browser/components/add-key/AddKeyReJSON/AddKeyReJSON.spec.tsx

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import React from 'react'
2+
import userEvent from '@testing-library/user-event'
23
import { instance, mock } from 'ts-mockito'
34

4-
import { fireEvent, render, screen } from 'uiSrc/utils/test-utils'
5+
import { fireEvent, render, screen, waitFor } from 'uiSrc/utils/test-utils'
6+
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
57

68
import AddKeyReJSON, { Props } from './AddKeyReJSON'
79
import AddKeyFooter from '../AddKeyFooter/AddKeyFooter'
@@ -14,6 +16,11 @@ jest.mock('../AddKeyFooter/AddKeyFooter', () => ({
1416
default: jest.fn()
1517
}))
1618

19+
jest.mock('uiSrc/telemetry', () => ({
20+
...jest.requireActual('uiSrc/telemetry'),
21+
sendEventTelemetry: jest.fn(),
22+
}))
23+
1724
const MockAddKeyFooter = (props: any) => (
1825
<div {...props} />
1926
)
@@ -61,4 +68,70 @@ describe('AddKeyReJSON', () => {
6168
)
6269
expect(screen.getByTestId('add-key-json-btn')).not.toBeDisabled()
6370
})
71+
72+
it('should call proper telemetry events after click Upload', () => {
73+
const sendEventTelemetryMock = jest.fn()
74+
sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock)
75+
76+
render(<AddKeyReJSON {...instance(mockedProps)} />)
77+
78+
fireEvent.click(screen.getByTestId('upload-input-file'))
79+
80+
expect(sendEventTelemetry).toBeCalledWith({
81+
event: TelemetryEvent.BROWSER_JSON_VALUE_IMPORT_CLICKED,
82+
eventData: {
83+
databaseId: 'instanceId',
84+
}
85+
})
86+
})
87+
88+
it('should load file', async () => {
89+
render(<AddKeyReJSON {...instance(mockedProps)} keyName="name" />)
90+
91+
const jsonString = JSON.stringify({ a: 12 })
92+
const blob = new Blob([jsonString])
93+
const file = new File([blob], 'empty.json', {
94+
type: 'application/JSON',
95+
})
96+
const fileInput = screen.getByTestId('upload-input-file')
97+
98+
expect(fileInput).toHaveAttribute('accept', 'application/json, text/plain')
99+
expect(fileInput.files.length).toBe(0)
100+
101+
await userEvent.upload(fileInput, file)
102+
103+
expect(fileInput.files.length).toBe(1)
104+
})
105+
106+
it('should set the value from json file', async () => {
107+
render(<AddKeyReJSON {...instance(mockedProps)} keyName="name" />)
108+
109+
const jsonString = JSON.stringify({ a: 12 })
110+
const blob = new Blob([jsonString])
111+
const file = new File([blob], 'empty.json', {
112+
type: 'application/JSON',
113+
})
114+
const fileInput = screen.getByTestId('upload-input-file')
115+
116+
expect(fileInput).toHaveAttribute('accept', 'application/json, text/plain')
117+
118+
await userEvent.upload(fileInput, file)
119+
120+
await waitFor(() => expect(screen.getByTestId('json-value')).toHaveValue('{"a":12}'))
121+
})
122+
123+
it('should set the incorrect json value from json file', async () => {
124+
render(<AddKeyReJSON {...instance(mockedProps)} keyName="name" />)
125+
126+
const jsonString = JSON.stringify('{ a: 12')
127+
const blob = new Blob([jsonString])
128+
const file = new File([blob], 'empty.json', {
129+
type: 'application/JSON',
130+
})
131+
const fileInput = screen.getByTestId('upload-input-file')
132+
133+
await userEvent.upload(fileInput, file)
134+
135+
await waitFor(() => expect(screen.getByTestId('json-value')).toHaveValue('"{ a: 12"'))
136+
})
64137
})

redisinsight/ui/src/pages/browser/components/add-key/AddKeyReJSON/AddKeyReJSON.tsx

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { FormEvent, useEffect, useState } from 'react'
22
import { useDispatch, useSelector } from 'react-redux'
3+
import { useParams } from 'react-router-dom'
34
import {
45
EuiButton,
56
EuiFormRow,
@@ -9,10 +10,13 @@ import {
910
EuiFlexItem,
1011
EuiPanel,
1112
} from '@elastic/eui'
13+
1214
import { Maybe, stringToBuffer } from 'uiSrc/utils'
1315
import { addKeyStateSelector, addReJSONKey, } from 'uiSrc/slices/browser/keys'
1416

1517
import MonacoJson from 'uiSrc/components/monaco-json'
18+
import UploadFile from 'uiSrc/components/uploadFile'
19+
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
1620
import { CreateRejsonRlWithExpireDto } from 'apiSrc/modules/browser/dto'
1721

1822
import {
@@ -31,10 +35,10 @@ const AddKeyReJSON = (props: Props) => {
3135
const { keyName = '', keyTTL, onCancel } = props
3236
const { loading } = useSelector(addKeyStateSelector)
3337
const [ReJSONValue, setReJSONValue] = useState<string>('')
34-
3538
const [isFormValid, setIsFormValid] = useState<boolean>(false)
3639

3740
const dispatch = useDispatch()
41+
const { instanceId } = useParams<{ instanceId: string }>()
3842

3943
useEffect(() => {
4044
try {
@@ -68,15 +72,41 @@ const AddKeyReJSON = (props: Props) => {
6872
dispatch(addReJSONKey(data, onCancel))
6973
}
7074

75+
const onFileChange = ({ target: { files } }: { target: { files: FileList | null } }) => {
76+
if (files && files[0]) {
77+
const reader = new FileReader()
78+
reader.onload = async (e) => {
79+
setReJSONValue(e?.target?.result as string)
80+
}
81+
reader.readAsText(files[0])
82+
}
83+
}
84+
85+
const onClick = () => {
86+
sendEventTelemetry({
87+
event: TelemetryEvent.BROWSER_JSON_VALUE_IMPORT_CLICKED,
88+
eventData: {
89+
databaseId: instanceId,
90+
}
91+
})
92+
}
93+
7194
return (
7295
<EuiForm component="form" onSubmit={onFormSubmit}>
7396
<EuiFormRow label={config.value.label} fullWidth>
74-
<MonacoJson
75-
value={ReJSONValue}
76-
onChange={setReJSONValue}
77-
disabled={loading}
78-
data-testid="json-value"
79-
/>
97+
<>
98+
<MonacoJson
99+
value={ReJSONValue}
100+
onChange={setReJSONValue}
101+
disabled={loading}
102+
data-testid="json-value"
103+
/>
104+
<EuiFlexGroup justifyContent="flexEnd">
105+
<EuiFlexItem grow={false}>
106+
<UploadFile onClick={onClick} onFileChange={onFileChange} />
107+
</EuiFlexItem>
108+
</EuiFlexGroup>
109+
</>
80110
</EuiFormRow>
81111

82112
<EuiButton type="submit" fill style={{ display: 'none' }}>

redisinsight/ui/src/telemetry/events.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export enum TelemetryEvent {
6262
BROWSER_JSON_PROPERTY_EDITED = 'BROWSER_JSON_PROPERTY_EDITED',
6363
BROWSER_JSON_PROPERTY_DELETED = 'BROWSER_JSON_PROPERTY_DELETED',
6464
BROWSER_JSON_PROPERTY_ADDED = 'BROWSER_JSON_PROPERTY_ADDED',
65+
BROWSER_JSON_VALUE_IMPORT_CLICKED = 'BROWSER_JSON_VALUE_IMPORT_CLICKED',
6566
BROWSER_KEYS_SCANNED = 'BROWSER_KEYS_SCANNED',
6667
BROWSER_KEYS_ADDITIONALLY_SCANNED = 'BROWSER_KEYS_ADDITIONALLY_SCANNED',
6768
BROWSER_KEYS_SCANNED_WITH_FILTER_ENABLED = 'BROWSER_KEYS_SCANNED_WITH_FILTER_ENABLED',

tests/e2e/pageObjects/browser-page.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ export class BrowserPage {
164164
listKeyElementEditorInput = Selector('[data-testid=element-value-editor]');
165165
stringKeyValueInput = Selector('[data-testid=string-value]');
166166
jsonKeyValueInput = Selector('[data-mode-id=json]');
167+
jsonUploadInput = Selector('[data-testid=upload-input-file]');
167168
setMemberInput = Selector('[data-testid=member-name]');
168169
zsetMemberScoreInput = Selector('[data-testid=member-score]');
169170
filterByPatterSearchInput = Selector('[data-testid=search-key]');
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"product": "Live JSON generator",
3+
"version": 3.1,
4+
"releaseDate": "2014-06-25T00:00:00.000Z",
5+
"demo": true,
6+
"person": {
7+
"id": 12345,
8+
"name": "John Doe",
9+
"phones": {
10+
"home": "800-123-4567",
11+
"mobile": "877-123-1234"
12+
},
13+
"email": [
14+
15+
16+
],
17+
"dateOfBirth": "1980-01-02T00:00:00.000Z",
18+
"registered": true,
19+
"emergencyContacts": [
20+
{
21+
"name": "Jane Doe",
22+
"phone": "888-555-1212",
23+
"relationship": "spouse"
24+
},
25+
{
26+
"name": "Justin Doe",
27+
"phone": "877-123-1212",
28+
"relationship": "parent"
29+
}
30+
]
31+
}
32+
}

0 commit comments

Comments
 (0)