Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react'
import { instance, mock } from 'ts-mockito'
import { render, screen, fireEvent } from 'uiSrc/utils/test-utils'
import PopoverItemEditor, { Props } from './PopoverItemEditor'

const mockedProps = mock<Props>()

describe('PopoverItemEditor', () => {
it('should render', () => {
expect(
render(
<PopoverItemEditor
{...instance(mockedProps)}
onDecline={jest.fn()}
>
<></>
</PopoverItemEditor>
)
).toBeTruthy()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import React, {
FormEvent,
useEffect,
useState,
} from 'react'

import {
EuiButton,
EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiForm,
EuiPopover,
} from '@elastic/eui'
import styles from './styles.module.scss'

export interface Props {
children: React.ReactElement
className?: string
onOpen: () => void
onApply: () => void
onDecline?: () => void
isLoading?: boolean
isDisabled?: boolean
declineOnUnmount?: boolean
btnTestId?: string
btnIconType?: string
}

const PopoverItemEditor = (props: Props) => {
const {
onOpen,
onDecline,
onApply,
children,
isLoading,
declineOnUnmount = true,
isDisabled,
btnTestId,
btnIconType,
className
} = props
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false)

useEffect(() =>
// componentWillUnmount
() => {
declineOnUnmount && onDecline?.()
},
[])

const onFormSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
handleApply()
}

const handleApply = (): void => {
setIsPopoverOpen(false)
onApply()
}

const handleDecline = () => {
setIsPopoverOpen(false)
onDecline?.()
}

const handleButtonClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation()
onOpen?.()
setIsPopoverOpen(true)
}

const isDisabledApply = (): boolean => !!(isLoading || isDisabled)

const button = (
<EuiButtonIcon
iconType={btnIconType || 'pencil'}
aria-label="Edit field"
color="primary"
disabled={isLoading}
onClick={handleButtonClick}
data-testid={btnTestId || 'popover-edit-bnt'}
/>
)

return (
<EuiPopover
ownFocus
anchorPosition="downLeft"
isOpen={isPopoverOpen}
anchorClassName={className}
panelClassName={styles.popoverWrapper}
closePopover={handleDecline}
button={button}
data-testid="popover-item-editor"
onClick={(e) => e.stopPropagation()}
>
<EuiForm component="form" onSubmit={onFormSubmit}>
<div className={styles.content}>
{children}
</div>
<EuiFlexGroup className={styles.footer} responsive={false} justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton color="secondary" onClick={() => handleDecline()} data-testid="cancel-btn">
Cancel
</EuiButton>
</EuiFlexItem>

<EuiFlexItem grow={false}>
<EuiButton
fill
type="submit"
color="secondary"
isDisabled={isDisabledApply()}
data-testid="save-btn"
>
Save
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiForm>
</EuiPopover>
)
}

export default PopoverItemEditor
3 changes: 3 additions & 0 deletions redisinsight/ui/src/components/popover-item-editor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import PopoverItemEditor from './PopoverItemEditor'

export default PopoverItemEditor
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.content {

}

.footer {
margin-top: 6px !important;
}
9 changes: 9 additions & 0 deletions redisinsight/ui/src/constants/texts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,12 @@ export const ScanNoResultsFoundText = (
</EuiText>
</>
)

export const lastDeliveredIDTooltipText = (
<>
Specify the ID of the last delivered entry in the stream from the new group's perspective.
<EuiSpacer size="xs" />
Otherwise, <b>$</b> represents the ID of the last entry in the stream,&nbsp;
<b>0</b> fetches the entire stream from the beginning.
</>
)
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import cx from 'classnames'
import React, { ChangeEvent, useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { lastDeliveredIDTooltipText } from 'uiSrc/constants/texts'

import { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys'
import { addNewGroupAction } from 'uiSrc/slices/browser/stream'
Expand Down Expand Up @@ -111,14 +112,7 @@ const AddStreamGroup = (props: Props) => {
className={styles.entryIdTooltip}
position="left"
title="Enter Valid ID, 0 or $"
content={(
<>
Specify the ID of the last delivered entry in the stream from the new group's perspective.
<EuiSpacer size="xs" />
Otherwise, <b>$</b> represents the ID of the last entry in the stream,&nbsp;
<b>0</b> fetches the entire stream from the beginning.
</>
)}
content={lastDeliveredIDTooltipText}
>
<EuiIcon type="iInCircle" style={{ cursor: 'pointer' }} />
</EuiToolTip>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ const PopoverDelete = (props: Props) => {
testid = '',
} = props

const onButtonClick = () => {
const onButtonClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation()
if (item + suffix !== deleting) {
showPopover(item)
handleButtonClick?.()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ export interface IConsumerGroup extends ConsumerGroupDto {
export interface Props {
data: IConsumerGroup[]
columns: ITableColumn[]
onEditGroup: (groupId:string, editing: boolean) => void
onClosePopover: () => void
onSelectGroup: ({ rowData }: { rowData: any }) => void
isFooterOpen?: boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,19 @@
.tooltip {
min-width: 325px;
}

.editLastId {
margin-right: 4px;
}

.idText, .error {
display: inline-block;
color: var(--euiColorMediumShade);
font: normal normal normal 12px/18px Graphik;
margin-top: 6px;
padding-right: 6px;
}

.error {
color: var(--euiColorDangerText);
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import { EuiText, EuiToolTip } from '@elastic/eui'
import { EuiFieldText, EuiIcon, EuiText, EuiToolTip } from '@elastic/eui'
import React, { useCallback, useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import PopoverItemEditor from 'uiSrc/components/popover-item-editor'
import { lastDeliveredIDTooltipText } from 'uiSrc/constants/texts'
import { selectedKeyDataSelector, updateSelectedKeyRefreshTime } from 'uiSrc/slices/browser/keys'

import {
streamGroupsSelector,
setSelectedGroup,
fetchConsumers,
setStreamViewType,
modifyLastDeliveredIdAction,
} from 'uiSrc/slices/browser/stream'
import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces'
import PopoverDelete from 'uiSrc/pages/browser/components/popover-delete/PopoverDelete'
import { consumerGroupIdRegex } from 'uiSrc/utils'
import { getFormatTime } from 'uiSrc/utils/streamUtils'
import { TableCellTextAlignment } from 'uiSrc/constants'
import { KeyTypes, TableCellTextAlignment } from 'uiSrc/constants'
import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'
import { StreamViewType } from 'uiSrc/slices/interfaces/stream'
import { updateSelectedKeyRefreshTime } from 'uiSrc/slices/browser/keys'
import { ConsumerGroupDto } from 'apiSrc/modules/browser/dto/stream.dto'

import { ConsumerGroupDto, UpdateConsumerGroupDto } from 'apiSrc/modules/browser/dto/stream.dto'

import GroupsView from './GroupsView'

Expand All @@ -26,7 +31,7 @@ export interface IConsumerGroup extends ConsumerGroupDto {
}

const suffix = '_stream_group'
const actionsWidth = 50
const actionsWidth = 80

export interface Props {
isFooterOpen: boolean
Expand All @@ -36,13 +41,18 @@ const GroupsViewWrapper = (props: Props) => {
const {
lastRefreshTime,
data: loadedGroups = [],
loading
} = useSelector(streamGroupsSelector)
const { name: key = '' } = useSelector(connectedInstanceSelector)
const { name: selectedKey } = useSelector(selectedKeyDataSelector) ?? {}

const dispatch = useDispatch()

const [groups, setGroups] = useState<IConsumerGroup[]>([])
const [deleting, setDeleting] = useState<string>('')
const [editValue, setEditValue] = useState<string>('')
const [idError, setIdError] = useState<string>('')
const [isIdFocused, setIsIdFocused] = useState<boolean>(false)

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

useEffect(() => {
if (!consumerGroupIdRegex.test(editValue)) {
setIdError('ID format is not correct')
return
}
setIdError('')
}, [editValue])

const closePopover = useCallback(() => {
setDeleting('')
}, [])
Expand Down Expand Up @@ -84,16 +102,6 @@ const GroupsViewWrapper = (props: Props) => {
// })
}

const handleEditGroup = (groupId = '', editing: boolean) => {
const newGroupsState = groups.map((item) => {
if (item.name === groupId) {
return { ...item, editing }
}
return item
})
setGroups(newGroupsState)
}

const handleSelectGroup = ({ rowData }: { rowData: any }) => {
dispatch(setSelectedGroup(rowData))
dispatch(fetchConsumers(
Expand All @@ -102,6 +110,17 @@ const GroupsViewWrapper = (props: Props) => {
))
}

const handleApplyEditId = (groupName: string) => {
if (!!groupName.length && !idError && selectedKey) {
const data: UpdateConsumerGroupDto = {
keyName: selectedKey,
name: groupName,
lastDeliveredId: editValue
}
dispatch(modifyLastDeliveredIdAction(data))
}
}

const columns: ITableColumn[] = [

{
Expand Down Expand Up @@ -188,9 +207,46 @@ const GroupsViewWrapper = (props: Props) => {
absoluteWidth: actionsWidth,
maxWidth: actionsWidth,
minWidth: actionsWidth,
render: function Actions(_act: any, { name }: ConsumerGroupDto) {
render: function Actions(_act: any, { lastDeliveredId, name }: ConsumerGroupDto) {
const showIdError = !isIdFocused && idError
return (
<div>
<PopoverItemEditor
btnTestId={`edit-stream-last-id-${lastDeliveredId}`}
onOpen={() => setEditValue(lastDeliveredId)}
onApply={() => handleApplyEditId(name)}
className={styles.editLastId}
isDisabled={!editValue.length || !!idError}
isLoading={loading}
>
<>
<EuiFieldText
fullWidth
name="id"
id="id"
placeholder="ID*"
value={editValue}
onChange={(e: any) => setEditValue(e.target.value)}
onBlur={() => setIsIdFocused(false)}
onFocus={() => setIsIdFocused(true)}
append={(
<EuiToolTip
anchorClassName="inputAppendIcon"
position="left"
title="Enter Valid ID, 0 or $"
content={lastDeliveredIDTooltipText}
>
<EuiIcon type="iInCircle" style={{ cursor: 'pointer' }} />
</EuiToolTip>
)}
style={{ width: 240 }}
autoComplete="off"
data-testid="last-id-field"
/>
{!showIdError && <span className={styles.idText} data-testid="id-help-text">Timestamp - Sequence Number or $</span>}
{showIdError && <span className={styles.error} data-testid="id-error">{idError}</span>}
</>
</PopoverItemEditor>
<PopoverDelete
text={(
<>
Expand Down Expand Up @@ -220,7 +276,6 @@ const GroupsViewWrapper = (props: Props) => {
<GroupsView
data={groups}
columns={columns}
onEditGroup={handleEditGroup}
onClosePopover={closePopover}
onSelectGroup={handleSelectGroup}
{...props}
Expand Down
Loading