Skip to content

Commit 17f6f5e

Browse files
authored
Merge pull request #3251 from RedisInsight/fe/feature/RI-4989-refactor-json_update
#RI-4989 - refactor json type
2 parents 4f4b70b + 5d8ca87 commit 17f6f5e

File tree

39 files changed

+1012
-1768
lines changed

39 files changed

+1012
-1768
lines changed

redisinsight/ui/src/pages/browser/components/popover-delete/PopoverDelete.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export interface Props {
99
text: JSX.Element | string
1010
item: string
1111
itemRaw?: RedisString
12-
suffix: string
12+
suffix?: string
1313
deleting: string
1414
closePopover: () => void
1515
showPopover: (item: string) => void
@@ -26,7 +26,7 @@ const PopoverDelete = (props: Props) => {
2626
text,
2727
item,
2828
itemRaw,
29-
suffix,
29+
suffix = '',
3030
deleting,
3131
closePopover,
3232
updateLoading,

redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/RejsonDetailsWrapper.tsx

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
import React from 'react'
1+
import React, { useState } from 'react'
22
import { useSelector } from 'react-redux'
33
import { EuiProgress } from '@elastic/eui'
44

5+
import { isUndefined } from 'lodash'
56
import { rejsonDataSelector, rejsonSelector } from 'uiSrc/slices/browser/rejson'
67
import { selectedKeyDataSelector, keysSelector } from 'uiSrc/slices/browser/keys'
78
import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'
89
import { sendEventTelemetry, TelemetryEvent, getBasedOnViewTypeEvent } from 'uiSrc/telemetry'
910
import { KeyDetailsHeader, KeyDetailsHeaderProps } from 'uiSrc/pages/browser/modules'
1011

1112
import { KeyTypes } from 'uiSrc/constants'
12-
import RejsonDetails from 'uiSrc/pages/browser/modules/key-details/components/rejson-details/rejson-details/RejsonDetails'
13+
import { stringToBuffer } from 'uiSrc/utils'
14+
import { IJSONData } from 'uiSrc/pages/browser/modules/key-details/components/rejson-details/interfaces'
15+
import RejsonDetails from './rejson-details/RejsonDetails'
1316

1417
import styles from './styles.module.scss'
1518

@@ -19,13 +22,11 @@ const RejsonDetailsWrapper = (props: Props) => {
1922
const keyType = KeyTypes.ReJSON
2023
const { loading } = useSelector(rejsonSelector)
2124
const { data, downloaded, type, path } = useSelector(rejsonDataSelector)
22-
const { name: selectedKey = '' } = useSelector(selectedKeyDataSelector) || {}
25+
const { name: selectedKey } = useSelector(selectedKeyDataSelector) || {}
2326
const { id: instanceId } = useSelector(connectedInstanceSelector)
2427
const { viewType } = useSelector(keysSelector)
2528

26-
const handleSubmitJsonUpdateValue = async () => {}
27-
28-
const handleEditValueUpdate = () => {}
29+
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set())
2930

3031
const reportJSONKeyCollapsed = (level: number) => {
3132
sendEventTelemetry({
@@ -63,6 +64,14 @@ const RejsonDetailsWrapper = (props: Props) => {
6364
} else {
6465
reportJSONKeyCollapsed(levelFromPath)
6566
}
67+
68+
setExpandedRows((rows) => {
69+
const copyOfSet = new Set(rows)
70+
if (isExpanded) copyOfSet.add(path)
71+
else copyOfSet.delete(path)
72+
73+
return copyOfSet
74+
})
6675
}
6776

6877
return (
@@ -73,35 +82,32 @@ const RejsonDetailsWrapper = (props: Props) => {
7382
keyType={keyType}
7483
/>
7584
<div className="key-details-body" key="key-details-body">
76-
{!loading && (
77-
<div className="flex-column" style={{ flex: '1', height: '100%' }}>
78-
<div
79-
data-testid="json-details"
80-
className={`${[styles.container].join(' ')}`}
81-
>
82-
{loading && (
83-
<EuiProgress
84-
color="primary"
85-
size="xs"
86-
position="absolute"
87-
data-testid="progress-key-json"
88-
/>
89-
)}
90-
{!(loading && data === undefined) && (
91-
<RejsonDetails
92-
selectedKey={selectedKey}
93-
dataType={type || ''}
94-
data={data}
95-
parentPath={path}
96-
onJsonKeyExpandAndCollapse={reportJsonKeyExpandAndCollapse}
97-
shouldRejsonDataBeDownloaded={!downloaded}
98-
handleSubmitJsonUpdateValue={handleSubmitJsonUpdateValue}
99-
handleSubmitUpdateValue={handleEditValueUpdate}
100-
/>
101-
)}
102-
</div>
85+
<div className="flex-column" style={{ flex: '1', height: '100%' }}>
86+
<div
87+
data-testid="json-details"
88+
className={styles.container}
89+
>
90+
{loading && (
91+
<EuiProgress
92+
color="primary"
93+
size="xs"
94+
position="absolute"
95+
data-testid="progress-key-json"
96+
/>
97+
)}
98+
{!isUndefined(data) && (
99+
<RejsonDetails
100+
selectedKey={selectedKey || stringToBuffer('')}
101+
dataType={type || ''}
102+
data={data as IJSONData}
103+
parentPath={path}
104+
expadedRows={expandedRows}
105+
onJsonKeyExpandAndCollapse={reportJsonKeyExpandAndCollapse}
106+
isDownloaded={downloaded}
107+
/>
108+
)}
103109
</div>
104-
)}
110+
</div>
105111
</div>
106112
</div>
107113
)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react'
22
import { instance, mock } from 'ts-mockito'
33
import { fireEvent, render, screen } from 'uiSrc/utils/test-utils'
4-
import { AddItemFieldAction, Props } from './AddItemFieldAction'
4+
import AddItemFieldAction, { Props } from './AddItemFieldAction'
55

66
const mockedProps = mock<Props>()
77

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 { EuiButtonIcon } from '@elastic/eui'
3+
import { getBrackets } from '../../utils'
4+
import styles from '../../styles.module.scss'
5+
6+
export interface Props {
7+
leftPadding: number
8+
type: string
9+
onClickSetKVPair: () => void
10+
}
11+
12+
const AddItemFieldAction = ({
13+
leftPadding,
14+
type,
15+
onClickSetKVPair,
16+
}: Props) => (
17+
<div
18+
className={styles.row}
19+
style={{ paddingLeft: `${leftPadding}em` }}
20+
>
21+
<span className={styles.defaultFont}>{getBrackets(type, 'end')}</span>
22+
<EuiButtonIcon
23+
iconType="plus"
24+
className={styles.jsonButtonStyle}
25+
onClick={onClickSetKVPair}
26+
aria-label="Add field"
27+
data-testid="add-field-btn"
28+
/>
29+
</div>
30+
)
31+
32+
export default AddItemFieldAction
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import AddItemFieldAction from './AddItemFieldAction'
2+
3+
export default AddItemFieldAction
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React from 'react'
2+
import { mock } from 'ts-mockito'
3+
import { fireEvent } from '@testing-library/react'
4+
import { render, screen } from 'uiSrc/utils/test-utils'
5+
6+
import AddItem, { Props } from './AddItem'
7+
import { JSONErrors } from '../../constants'
8+
9+
const mockedProps = mock<Props>()
10+
11+
describe('AddItem', () => {
12+
it('should render', () => {
13+
expect(render(<AddItem {...mockedProps} />)).toBeTruthy()
14+
})
15+
16+
it('should show error with invalid key', () => {
17+
render(<AddItem {...mockedProps} isPair onCancel={jest.fn} />)
18+
19+
fireEvent.change(screen.getByTestId('json-key'), { target: { value: '"' } })
20+
fireEvent.click(screen.getByTestId('apply-btn'))
21+
22+
expect(screen.getByTestId('edit-json-error')).toHaveTextContent(JSONErrors.keyCorrectSyntax)
23+
})
24+
25+
it('should show error with invalid value', () => {
26+
render(<AddItem {...mockedProps} onCancel={jest.fn} />)
27+
28+
expect(screen.queryByTestId('json-key')).not.toBeInTheDocument()
29+
30+
fireEvent.change(screen.getByTestId('json-value'), { target: { value: '"' } })
31+
fireEvent.click(screen.getByTestId('apply-btn'))
32+
33+
expect(screen.getByTestId('edit-json-error')).toHaveTextContent(JSONErrors.valueJSONFormat)
34+
})
35+
36+
it('should submit with proper key and value', () => {
37+
const onSubmit = jest.fn()
38+
render(<AddItem {...mockedProps} isPair onCancel={jest.fn} onSubmit={onSubmit} />)
39+
40+
fireEvent.change(screen.getByTestId('json-key'), { target: { value: '"key"' } })
41+
fireEvent.change(screen.getByTestId('json-value'), { target: { value: '1' } })
42+
fireEvent.click(screen.getByTestId('apply-btn'))
43+
44+
expect(onSubmit).toBeCalledWith({ key: '"key"', value: '1' })
45+
})
46+
})
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import React, { useEffect, useState } from 'react'
2+
import cx from 'classnames'
3+
import {
4+
EuiButtonIcon,
5+
EuiFieldText,
6+
EuiFlexItem,
7+
EuiFocusTrap,
8+
EuiForm,
9+
EuiOutsideClickDetector,
10+
EuiWindowEvent,
11+
keys
12+
} from '@elastic/eui'
13+
14+
import FieldMessage from 'uiSrc/components/field-message/FieldMessage'
15+
import { Nullable } from 'uiSrc/utils'
16+
import { isValidJSON, isValidKey } from '../../utils'
17+
import { JSONErrors } from '../../constants'
18+
19+
import styles from '../../styles.module.scss'
20+
21+
export interface Props {
22+
isPair: boolean
23+
onCancel: () => void
24+
onSubmit: (pair: { key?: string, value: string }) => void
25+
leftPadding?: number
26+
}
27+
28+
const AddItem = (props: Props) => {
29+
const {
30+
isPair,
31+
leftPadding = 0,
32+
onCancel,
33+
onSubmit
34+
} = props
35+
36+
const [key, setKey] = useState<string>('')
37+
const [value, setValue] = useState<string>('')
38+
const [error, setError] = useState<Nullable<string>>(null)
39+
40+
useEffect(() => {
41+
setError(null)
42+
}, [key, value])
43+
44+
const handleOnEsc = (e: KeyboardEvent) => {
45+
if (e.code?.toLowerCase() === keys.ESCAPE) {
46+
e.stopPropagation()
47+
onCancel?.()
48+
}
49+
}
50+
51+
const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
52+
e.preventDefault()
53+
54+
if (isPair && !isValidKey(key)) {
55+
setError(JSONErrors.keyCorrectSyntax)
56+
return
57+
}
58+
59+
if (!isValidJSON(value)) {
60+
setError(JSONErrors.valueJSONFormat)
61+
return
62+
}
63+
64+
onSubmit({ key, value })
65+
}
66+
67+
return (
68+
<div className={styles.row} style={{ display: 'flex', flexDirection: 'row', paddingLeft: `${leftPadding}em` }}>
69+
<EuiOutsideClickDetector onOutsideClick={onCancel}>
70+
<div>
71+
<EuiWindowEvent event="keydown" handler={(e) => handleOnEsc(e)} />
72+
<EuiFocusTrap>
73+
<EuiForm
74+
component="form"
75+
className="relative"
76+
onSubmit={(e) => handleFormSubmit(e)}
77+
style={{ display: 'flex' }}
78+
noValidate
79+
>
80+
{isPair && (
81+
<EuiFlexItem grow component="span">
82+
<EuiFieldText
83+
name="newRootKey"
84+
value={key}
85+
isInvalid={!!error}
86+
placeholder="Enter JSON key"
87+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setKey(e.target.value)}
88+
data-testid="json-key"
89+
/>
90+
</EuiFlexItem>
91+
)}
92+
<EuiFlexItem grow component="span">
93+
<EuiFieldText
94+
name="newValue"
95+
value={value}
96+
placeholder="Enter JSON value"
97+
isInvalid={!!error}
98+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value)}
99+
data-testid="json-value"
100+
/>
101+
</EuiFlexItem>
102+
<div className={cx(styles.controls)}>
103+
<EuiButtonIcon
104+
iconSize="m"
105+
iconType="cross"
106+
color="primary"
107+
aria-label="Cancel editing"
108+
className={styles.declineBtn}
109+
onClick={() => onCancel?.()}
110+
/>
111+
<EuiButtonIcon
112+
iconSize="m"
113+
iconType="check"
114+
color="primary"
115+
type="submit"
116+
aria-label="Apply"
117+
className={styles.applyBtn}
118+
data-testid="apply-btn"
119+
/>
120+
</div>
121+
</EuiForm>
122+
{!!error && (
123+
<div className={cx(styles.errorMessage)}>
124+
<FieldMessage
125+
scrollViewOnAppear
126+
icon="alert"
127+
testID="edit-json-error"
128+
>
129+
{error}
130+
</FieldMessage>
131+
</div>
132+
)}
133+
</EuiFocusTrap>
134+
</div>
135+
</EuiOutsideClickDetector>
136+
</div>
137+
)
138+
}
139+
140+
export default AddItem
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import AddItem from './AddItem'
2+
3+
export default AddItem

redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/components/add-item/styles.module.scss

Whitespace-only changes.

0 commit comments

Comments
 (0)