Skip to content

Commit 7297810

Browse files
committed
#RI-4989 - refactor json type
1 parent 4f4b70b commit 7297810

File tree

39 files changed

+997
-1767
lines changed

39 files changed

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