Skip to content

Commit 90c5e92

Browse files
committed
Merge remote-tracking branch 'origin/feature/RI-2932_Consumer_Groups' into feature/RI-2932_Consumer_Groups
2 parents 32071c0 + aa08a73 commit 90c5e92

File tree

11 files changed

+308
-27
lines changed

11 files changed

+308
-27
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from 'react'
2+
import { instance, mock } from 'ts-mockito'
3+
import { render, screen, fireEvent } from 'uiSrc/utils/test-utils'
4+
import PopoverItemEditor, { Props } from './PopoverItemEditor'
5+
6+
const mockedProps = mock<Props>()
7+
8+
describe('PopoverItemEditor', () => {
9+
it('should render', () => {
10+
expect(
11+
render(
12+
<PopoverItemEditor
13+
{...instance(mockedProps)}
14+
onDecline={jest.fn()}
15+
>
16+
<></>
17+
</PopoverItemEditor>
18+
)
19+
).toBeTruthy()
20+
})
21+
})
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import React, {
2+
FormEvent,
3+
useEffect,
4+
useState,
5+
} from 'react'
6+
7+
import {
8+
EuiButton,
9+
EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiForm,
10+
EuiPopover,
11+
} from '@elastic/eui'
12+
import styles from './styles.module.scss'
13+
14+
export interface Props {
15+
children: React.ReactElement
16+
className?: string
17+
onOpen: () => void
18+
onApply: () => void
19+
onDecline?: () => void
20+
isLoading?: boolean
21+
isDisabled?: boolean
22+
declineOnUnmount?: boolean
23+
btnTestId?: string
24+
btnIconType?: string
25+
}
26+
27+
const PopoverItemEditor = (props: Props) => {
28+
const {
29+
onOpen,
30+
onDecline,
31+
onApply,
32+
children,
33+
isLoading,
34+
declineOnUnmount = true,
35+
isDisabled,
36+
btnTestId,
37+
btnIconType,
38+
className
39+
} = props
40+
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false)
41+
42+
useEffect(() =>
43+
// componentWillUnmount
44+
() => {
45+
declineOnUnmount && onDecline?.()
46+
},
47+
[])
48+
49+
const onFormSubmit = (e: FormEvent<HTMLFormElement>) => {
50+
e.preventDefault()
51+
handleApply()
52+
}
53+
54+
const handleApply = (): void => {
55+
setIsPopoverOpen(false)
56+
onApply()
57+
}
58+
59+
const handleDecline = () => {
60+
setIsPopoverOpen(false)
61+
onDecline?.()
62+
}
63+
64+
const handleButtonClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
65+
e.stopPropagation()
66+
onOpen?.()
67+
setIsPopoverOpen(true)
68+
}
69+
70+
const isDisabledApply = (): boolean => !!(isLoading || isDisabled)
71+
72+
const button = (
73+
<EuiButtonIcon
74+
iconType={btnIconType || 'pencil'}
75+
aria-label="Edit field"
76+
color="primary"
77+
disabled={isLoading}
78+
onClick={handleButtonClick}
79+
data-testid={btnTestId || 'popover-edit-bnt'}
80+
/>
81+
)
82+
83+
return (
84+
<EuiPopover
85+
ownFocus
86+
anchorPosition="downLeft"
87+
isOpen={isPopoverOpen}
88+
anchorClassName={className}
89+
panelClassName={styles.popoverWrapper}
90+
closePopover={handleDecline}
91+
button={button}
92+
data-testid="popover-item-editor"
93+
onClick={(e) => e.stopPropagation()}
94+
>
95+
<EuiForm component="form" onSubmit={onFormSubmit}>
96+
<div className={styles.content}>
97+
{children}
98+
</div>
99+
<EuiFlexGroup className={styles.footer} responsive={false} justifyContent="flexEnd">
100+
<EuiFlexItem grow={false}>
101+
<EuiButton color="secondary" onClick={() => handleDecline()} data-testid="cancel-btn">
102+
Cancel
103+
</EuiButton>
104+
</EuiFlexItem>
105+
106+
<EuiFlexItem grow={false}>
107+
<EuiButton
108+
fill
109+
type="submit"
110+
color="secondary"
111+
isDisabled={isDisabledApply()}
112+
data-testid="save-btn"
113+
>
114+
Save
115+
</EuiButton>
116+
</EuiFlexItem>
117+
</EuiFlexGroup>
118+
</EuiForm>
119+
</EuiPopover>
120+
)
121+
}
122+
123+
export default PopoverItemEditor
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import PopoverItemEditor from './PopoverItemEditor'
2+
3+
export default PopoverItemEditor
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.content {
2+
3+
}
4+
5+
.footer {
6+
margin-top: 6px !important;
7+
}

redisinsight/ui/src/constants/texts.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,12 @@ export const ScanNoResultsFoundText = (
2525
</EuiText>
2626
</>
2727
)
28+
29+
export const lastDeliveredIDTooltipText = (
30+
<>
31+
Specify the ID of the last delivered entry in the stream from the new group's perspective.
32+
<EuiSpacer size="xs" />
33+
Otherwise, <b>$</b> represents the ID of the last entry in the stream,&nbsp;
34+
<b>0</b> fetches the entire stream from the beginning.
35+
</>
36+
)

redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/AddStreamGroup.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import cx from 'classnames'
1414
import React, { ChangeEvent, useEffect, useState } from 'react'
1515
import { useDispatch, useSelector } from 'react-redux'
16+
import { lastDeliveredIDTooltipText } from 'uiSrc/constants/texts'
1617

1718
import { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys'
1819
import { addNewGroupAction } from 'uiSrc/slices/browser/stream'
@@ -111,14 +112,7 @@ const AddStreamGroup = (props: Props) => {
111112
className={styles.entryIdTooltip}
112113
position="left"
113114
title="Enter Valid ID, 0 or $"
114-
content={(
115-
<>
116-
Specify the ID of the last delivered entry in the stream from the new group's perspective.
117-
<EuiSpacer size="xs" />
118-
Otherwise, <b>$</b> represents the ID of the last entry in the stream,&nbsp;
119-
<b>0</b> fetches the entire stream from the beginning.
120-
</>
121-
)}
115+
content={lastDeliveredIDTooltipText}
122116
>
123117
<EuiIcon type="iInCircle" style={{ cursor: 'pointer' }} />
124118
</EuiToolTip>

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ const PopoverDelete = (props: Props) => {
3434
testid = '',
3535
} = props
3636

37-
const onButtonClick = () => {
37+
const onButtonClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
38+
e.stopPropagation()
3839
if (item + suffix !== deleting) {
3940
showPopover(item)
4041
handleButtonClick?.()

redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/GroupsView.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ export interface IConsumerGroup extends ConsumerGroupDto {
2525
export interface Props {
2626
data: IConsumerGroup[]
2727
columns: ITableColumn[]
28-
onEditGroup: (groupId:string, editing: boolean) => void
2928
onClosePopover: () => void
3029
onSelectGroup: ({ rowData }: { rowData: any }) => void
3130
isFooterOpen?: boolean

redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/styles.module.scss

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,19 @@
1414
.tooltip {
1515
min-width: 325px;
1616
}
17+
18+
.editLastId {
19+
margin-right: 4px;
20+
}
21+
22+
.idText, .error {
23+
display: inline-block;
24+
color: var(--euiColorMediumShade);
25+
font: normal normal normal 12px/18px Graphik;
26+
margin-top: 6px;
27+
padding-right: 6px;
28+
}
29+
30+
.error {
31+
color: var(--euiColorDangerText);
32+
}

redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsViewWrapper.tsx

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
1-
import { EuiText, EuiToolTip } from '@elastic/eui'
1+
import { EuiFieldText, EuiIcon, EuiText, EuiToolTip } from '@elastic/eui'
22
import React, { useCallback, useEffect, useState } from 'react'
33
import { useDispatch, useSelector } from 'react-redux'
4+
import PopoverItemEditor from 'uiSrc/components/popover-item-editor'
5+
import { lastDeliveredIDTooltipText } from 'uiSrc/constants/texts'
6+
import { selectedKeyDataSelector, updateSelectedKeyRefreshTime } from 'uiSrc/slices/browser/keys'
47

58
import {
69
streamGroupsSelector,
710
setSelectedGroup,
811
fetchConsumers,
912
setStreamViewType,
13+
modifyLastDeliveredIdAction,
1014
} from 'uiSrc/slices/browser/stream'
1115
import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces'
1216
import PopoverDelete from 'uiSrc/pages/browser/components/popover-delete/PopoverDelete'
17+
import { consumerGroupIdRegex } from 'uiSrc/utils'
1318
import { getFormatTime } from 'uiSrc/utils/streamUtils'
14-
import { TableCellTextAlignment } from 'uiSrc/constants'
19+
import { KeyTypes, TableCellTextAlignment } from 'uiSrc/constants'
1520
import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'
1621
import { StreamViewType } from 'uiSrc/slices/interfaces/stream'
17-
import { updateSelectedKeyRefreshTime } from 'uiSrc/slices/browser/keys'
18-
import { ConsumerGroupDto } from 'apiSrc/modules/browser/dto/stream.dto'
22+
23+
import { ConsumerGroupDto, UpdateConsumerGroupDto } from 'apiSrc/modules/browser/dto/stream.dto'
1924

2025
import GroupsView from './GroupsView'
2126

@@ -26,7 +31,7 @@ export interface IConsumerGroup extends ConsumerGroupDto {
2631
}
2732

2833
const suffix = '_stream_group'
29-
const actionsWidth = 50
34+
const actionsWidth = 80
3035

3136
export interface Props {
3237
isFooterOpen: boolean
@@ -36,13 +41,18 @@ const GroupsViewWrapper = (props: Props) => {
3641
const {
3742
lastRefreshTime,
3843
data: loadedGroups = [],
44+
loading
3945
} = useSelector(streamGroupsSelector)
4046
const { name: key = '' } = useSelector(connectedInstanceSelector)
47+
const { name: selectedKey } = useSelector(selectedKeyDataSelector) ?? {}
4148

4249
const dispatch = useDispatch()
4350

4451
const [groups, setGroups] = useState<IConsumerGroup[]>([])
4552
const [deleting, setDeleting] = useState<string>('')
53+
const [editValue, setEditValue] = useState<string>('')
54+
const [idError, setIdError] = useState<string>('')
55+
const [isIdFocused, setIsIdFocused] = useState<boolean>(false)
4656

4757
useEffect(() => {
4858
dispatch(updateSelectedKeyRefreshTime(lastRefreshTime))
@@ -57,6 +67,14 @@ const GroupsViewWrapper = (props: Props) => {
5767
setGroups(streamItem)
5868
}, [loadedGroups, deleting])
5969

70+
useEffect(() => {
71+
if (!consumerGroupIdRegex.test(editValue)) {
72+
setIdError('ID format is not correct')
73+
return
74+
}
75+
setIdError('')
76+
}, [editValue])
77+
6078
const closePopover = useCallback(() => {
6179
setDeleting('')
6280
}, [])
@@ -84,16 +102,6 @@ const GroupsViewWrapper = (props: Props) => {
84102
// })
85103
}
86104

87-
const handleEditGroup = (groupId = '', editing: boolean) => {
88-
const newGroupsState = groups.map((item) => {
89-
if (item.name === groupId) {
90-
return { ...item, editing }
91-
}
92-
return item
93-
})
94-
setGroups(newGroupsState)
95-
}
96-
97105
const handleSelectGroup = ({ rowData }: { rowData: any }) => {
98106
dispatch(setSelectedGroup(rowData))
99107
dispatch(fetchConsumers(
@@ -102,6 +110,17 @@ const GroupsViewWrapper = (props: Props) => {
102110
))
103111
}
104112

113+
const handleApplyEditId = (groupName: string) => {
114+
if (!!groupName.length && !idError && selectedKey) {
115+
const data: UpdateConsumerGroupDto = {
116+
keyName: selectedKey,
117+
name: groupName,
118+
lastDeliveredId: editValue
119+
}
120+
dispatch(modifyLastDeliveredIdAction(data))
121+
}
122+
}
123+
105124
const columns: ITableColumn[] = [
106125

107126
{
@@ -188,9 +207,46 @@ const GroupsViewWrapper = (props: Props) => {
188207
absoluteWidth: actionsWidth,
189208
maxWidth: actionsWidth,
190209
minWidth: actionsWidth,
191-
render: function Actions(_act: any, { name }: ConsumerGroupDto) {
210+
render: function Actions(_act: any, { lastDeliveredId, name }: ConsumerGroupDto) {
211+
const showIdError = !isIdFocused && idError
192212
return (
193213
<div>
214+
<PopoverItemEditor
215+
btnTestId={`edit-stream-last-id-${lastDeliveredId}`}
216+
onOpen={() => setEditValue(lastDeliveredId)}
217+
onApply={() => handleApplyEditId(name)}
218+
className={styles.editLastId}
219+
isDisabled={!editValue.length || !!idError}
220+
isLoading={loading}
221+
>
222+
<>
223+
<EuiFieldText
224+
fullWidth
225+
name="id"
226+
id="id"
227+
placeholder="ID*"
228+
value={editValue}
229+
onChange={(e: any) => setEditValue(e.target.value)}
230+
onBlur={() => setIsIdFocused(false)}
231+
onFocus={() => setIsIdFocused(true)}
232+
append={(
233+
<EuiToolTip
234+
anchorClassName="inputAppendIcon"
235+
position="left"
236+
title="Enter Valid ID, 0 or $"
237+
content={lastDeliveredIDTooltipText}
238+
>
239+
<EuiIcon type="iInCircle" style={{ cursor: 'pointer' }} />
240+
</EuiToolTip>
241+
)}
242+
style={{ width: 240 }}
243+
autoComplete="off"
244+
data-testid="last-id-field"
245+
/>
246+
{!showIdError && <span className={styles.idText} data-testid="id-help-text">Timestamp - Sequence Number or $</span>}
247+
{showIdError && <span className={styles.error} data-testid="id-error">{idError}</span>}
248+
</>
249+
</PopoverItemEditor>
194250
<PopoverDelete
195251
text={(
196252
<>
@@ -220,7 +276,6 @@ const GroupsViewWrapper = (props: Props) => {
220276
<GroupsView
221277
data={groups}
222278
columns={columns}
223-
onEditGroup={handleEditGroup}
224279
onClosePopover={closePopover}
225280
onSelectGroup={handleSelectGroup}
226281
{...props}

0 commit comments

Comments
 (0)