From 9809b19e27e401f24a8f42d1109f806edf3cc181 Mon Sep 17 00:00:00 2001 From: "Roman.Sergeenko" Date: Mon, 23 May 2022 14:14:33 +0400 Subject: [PATCH 01/20] #RI-810 - initial implementation of add consumer groups --- redisinsight/ui/src/constants/api.ts | 3 +- redisinsight/ui/src/constants/keys.ts | 15 +- .../add-stream-group/AddStreamGroup.spec.tsx | 65 +++++++ .../add-stream-group/AddStreamGroup.tsx | 170 ++++++++++++++++++ .../add-stream-group/index.ts | 3 + .../add-stream-group/styles.module.scss | 30 ++++ .../components/key-details-add-items/index.ts | 4 +- .../key-details-header/KeyDetailsHeader.tsx | 58 ++++-- .../key-details/KeyDetails/KeyDetails.tsx | 16 +- redisinsight/ui/src/slices/browser/stream.ts | 53 +++++- .../ui/src/slices/interfaces/stream.ts | 8 +- redisinsight/ui/src/utils/validations.ts | 2 + 12 files changed, 402 insertions(+), 25 deletions(-) create mode 100644 redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/AddStreamGroup.spec.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/AddStreamGroup.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/index.ts create mode 100644 redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/styles.module.scss diff --git a/redisinsight/ui/src/constants/api.ts b/redisinsight/ui/src/constants/api.ts index eef3fb81be..68e8f321ca 100644 --- a/redisinsight/ui/src/constants/api.ts +++ b/redisinsight/ui/src/constants/api.ts @@ -33,9 +33,10 @@ enum ApiEndpoints { REJSON_GET = 'rejson-rl/get', REJSON_SET = 'rejson-rl/set', REJSON_ARRAPPEND = 'rejson-rl/arrappend', + STREAMS = 'streams', STREAMS_ENTRIES = 'streams/entries', STREAMS_ENTRIES_GET = 'streams/entries/get', - STREAMS = 'streams', + STREAMS_CONSUMER_GROUPS = 'streams/consumer-groups', CLI = 'cli', CLI_BLOCKING_COMMANDS = 'info/cli-blocking-commands', CLI_UNSUPPORTED_COMMANDS = 'info/cli-unsupported-commands', diff --git a/redisinsight/ui/src/constants/keys.ts b/redisinsight/ui/src/constants/keys.ts index 98a946e04b..15c549f045 100644 --- a/redisinsight/ui/src/constants/keys.ts +++ b/redisinsight/ui/src/constants/keys.ts @@ -1,3 +1,4 @@ +import { StreamViewType } from 'uiSrc/slices/interfaces/stream' import { CommandGroup } from './commands' export enum KeyTypes { @@ -114,11 +115,15 @@ export const KEY_TYPES_ACTIONS: KeyTypesActions = Object.freeze({ name: 'Edit Value', }, }, - [KeyTypes.ReJSON]: {}, - [KeyTypes.Stream]: { - addItems: { - name: 'New Entry', - }, + [KeyTypes.ReJSON]: {} +}) + +export const STREAM_ADD_ACTION = Object.freeze({ + [StreamViewType.Streams]: { + name: 'New Entry' + }, + [StreamViewType.Groups]: { + name: 'New Group' } }) diff --git a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/AddStreamGroup.spec.tsx b/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/AddStreamGroup.spec.tsx new file mode 100644 index 0000000000..b0c40cd795 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/AddStreamGroup.spec.tsx @@ -0,0 +1,65 @@ +import React from 'react' +import { fireEvent, render, screen } from 'uiSrc/utils/test-utils' +import { instance, mock } from 'ts-mockito' +import AddStreamGroup, { Props } from './AddStreamGroup' + +const GROUP_NAME_FIELD = 'group-name-field' +const ID_FIELD = 'id-field' + +const mockedProps = mock() + +describe('AddStreamGroup', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should set member value properly', () => { + render() + const groupNameInput = screen.getByTestId(GROUP_NAME_FIELD) + fireEvent.change( + groupNameInput, + { target: { value: 'group name' } } + ) + expect(groupNameInput).toHaveValue('group name') + }) + + it('should set score value properly if input wrong value', () => { + render() + const idInput = screen.getByTestId(ID_FIELD) + fireEvent.change( + idInput, + { target: { value: 'aa1x-5' } } + ) + expect(idInput).toHaveValue('1-5') + }) + + it('should able to save with valid data', () => { + render() + const groupNameInput = screen.getByTestId(GROUP_NAME_FIELD) + const idInput = screen.getByTestId(ID_FIELD) + fireEvent.change( + groupNameInput, + { target: { value: 'name' } } + ) + fireEvent.change( + idInput, + { target: { value: '11111-3' } } + ) + expect(screen.getByTestId('save-groups-btn')).not.toBeDisabled() + }) + + it('should not able to save with valid data', () => { + render() + const groupNameInput = screen.getByTestId(GROUP_NAME_FIELD) + const idInput = screen.getByTestId(ID_FIELD) + fireEvent.change( + groupNameInput, + { target: { value: 'name' } } + ) + fireEvent.change( + idInput, + { target: { value: '11111----' } } + ) + expect(screen.getByTestId('save-groups-btn')).toBeDisabled() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/AddStreamGroup.tsx b/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/AddStreamGroup.tsx new file mode 100644 index 0000000000..381faa628d --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/AddStreamGroup.tsx @@ -0,0 +1,170 @@ +import { + EuiButton, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiIcon, + EuiPanel, + EuiSpacer, + EuiTextColor, + EuiToolTip +} from '@elastic/eui' +import cx from 'classnames' +import React, { ChangeEvent, useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys' +import { addNewGroupAction } from 'uiSrc/slices/browser/stream' +import { consumerGroupIdRegex, validateConsumerGroupId } from 'uiSrc/utils' + +import styles from './styles.module.scss' + +export interface Props { + onCancel: (isCancelled?: boolean) => void +} + +const AddStreamGroup = (props: Props) => { + const { onCancel } = props + const { name: keyName = '' } = useSelector(selectedKeyDataSelector) ?? { name: undefined } + + const [isFormValid, setIsFormValid] = useState(false) + const [groupName, setGroupName] = useState('') + const [id, setId] = useState('$') + const [idError, setIdError] = useState('') + const [isIdFocused, setIsIdFocused] = useState(false) + + const dispatch = useDispatch() + + useEffect(() => { + const isValid = !!groupName.length && !idError + setIsFormValid(isValid) + }, [groupName, idError]) + + useEffect(() => { + if (!consumerGroupIdRegex.test(id)) { + setIdError('ID format is not correct') + return + } + setIdError('') + }, [id]) + + const submitData = () => { + if (isFormValid) { + const data: any = { + keyName, + consumerGroups: [{ + name: groupName, + lastDeliveredId: id, + }], + } + dispatch(addNewGroupAction(data, onCancel)) + } + } + + const showIdError = !isIdFocused && idError + + return ( + <> + + + + + + + + ) => setGroupName(e.target.value)} + autoComplete="off" + data-testid="group-name-field" + /> + + + + + ) => setId(validateConsumerGroupId(e.target.value))} + onBlur={() => setIsIdFocused(false)} + onFocus={() => setIsIdFocused(true)} + append={( + + Specify the ID of the last delivered entry in the stream from the new group's perspective. + + Otherwise, $ represents the ID of the last entry in the stream,  + 0 fetches the entire stream from the beginning. + + )} + > + + + )} + autoComplete="off" + data-testid="id-field" + /> + + {!showIdError && Timestamp - Sequence Number or $} + {showIdError && {idError}} + + + + + + + + + +
+ onCancel(true)} data-testid="cancel-stream-groups-btn"> + Cancel + +
+
+ +
+ + Save + +
+
+
+
+ + ) +} + +export default AddStreamGroup diff --git a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/index.ts b/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/index.ts new file mode 100644 index 0000000000..cdce9fc4da --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/index.ts @@ -0,0 +1,3 @@ +import AddStreamGroup from './AddStreamGroup' + +export default AddStreamGroup diff --git a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/styles.module.scss b/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/styles.module.scss new file mode 100644 index 0000000000..3536c4aecb --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/styles.module.scss @@ -0,0 +1,30 @@ +.content { + display: flex; + flex-direction: column; + width: 100%; + border: none !important; + border-top: 1px solid var(--euiColorPrimary); + padding: 12px 20px; + max-height: 234px; + scroll-padding-bottom: 30px; + + .groupNameWrapper { + flex-grow: 2 !important; + } + + .timestampWrapper { + min-width: 215px; + } + + .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); + } +} diff --git a/redisinsight/ui/src/pages/browser/components/key-details-add-items/index.ts b/redisinsight/ui/src/pages/browser/components/key-details-add-items/index.ts index a37d080cac..c389c49cbe 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details-add-items/index.ts +++ b/redisinsight/ui/src/pages/browser/components/key-details-add-items/index.ts @@ -2,6 +2,7 @@ import AddHashFields from './add-hash-fields/AddHashFields' import AddListElements from './add-list-elements/AddListElements' import AddSetMembers from './add-set-members/AddSetMembers' import AddStreamEntries, { StreamEntryFields } from './add-stream-entity' +import AddStreamGroup from './add-stream-group' import AddZsetMembers from './add-zset-members/AddZsetMembers' export { @@ -10,5 +11,6 @@ export { AddSetMembers, AddStreamEntries, StreamEntryFields, - AddZsetMembers + AddZsetMembers, + AddStreamGroup } diff --git a/redisinsight/ui/src/pages/browser/components/key-details-header/KeyDetailsHeader.tsx b/redisinsight/ui/src/pages/browser/components/key-details-header/KeyDetailsHeader.tsx index 742140a210..5ee524aee0 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details-header/KeyDetailsHeader.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-details-header/KeyDetailsHeader.tsx @@ -6,25 +6,26 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, + EuiLoadingContent, EuiPopover, EuiText, EuiToolTip, - EuiLoadingContent, } from '@elastic/eui' +import cx from 'classnames' +import { isNull } from 'lodash' import React, { ChangeEvent, useEffect, useRef, useState } from 'react' import { useSelector } from 'react-redux' -import { isNull } from 'lodash' -import cx from 'classnames' import AutoSizer from 'react-virtualized-auto-sizer' import { GroupBadge } from 'uiSrc/components' -import { KeyTypes, KEY_TYPES_ACTIONS, LENGTH_NAMING_BY_TYPE, ModulesKeyTypes } from 'uiSrc/constants' -import { selectedKeyDataSelector, selectedKeySelector, keysSelector } from 'uiSrc/slices/browser/keys' +import InlineItemEditor from 'uiSrc/components/inline-item-editor/InlineItemEditor' +import { KEY_TYPES_ACTIONS, KeyTypes, LENGTH_NAMING_BY_TYPE, ModulesKeyTypes, STREAM_ADD_ACTION } from 'uiSrc/constants' +import { AddCommonFieldsFormConfig } from 'uiSrc/pages/browser/components/add-key/constants/fields-config' +import { keysSelector, selectedKeyDataSelector, selectedKeySelector } from 'uiSrc/slices/browser/keys' +import { streamSelector } from 'uiSrc/slices/browser/stream' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { formatBytes, formatNameShort, MAX_TTL_NUMBER, replaceSpaces, validateTTLNumber } from 'uiSrc/utils' -import { sendEventTelemetry, TelemetryEvent, getBasedOnViewTypeEvent } from 'uiSrc/telemetry' -import { AddCommonFieldsFormConfig } from 'uiSrc/pages/browser/components/add-key/constants/fields-config' -import InlineItemEditor from 'uiSrc/components/inline-item-editor/InlineItemEditor' import AutoRefresh from '../auto-refresh' import styles from './styles.module.scss' @@ -75,6 +76,7 @@ const KeyDetailsHeader = ({ const { ttl: ttlProp, name: keyProp = '', type, size, length } = useSelector(selectedKeyDataSelector) ?? initialKeyInfo const { id: instanceId } = useSelector(connectedInstanceSelector) const { viewType } = useSelector(keysSelector) + const { viewType: streamViewType } = useSelector(streamSelector) const [isPopoverDeleteOpen, setIsPopoverDeleteOpen] = useState(false) @@ -93,7 +95,7 @@ const KeyDetailsHeader = ({ const keyNameRef = useRef(null) - const tooltipContent = formatNameShort(keyProp) + const tooltipContent = formatNameShort(keyProp || '') const onMouseEnterKey = () => { setKeyIsHovering(true) @@ -266,7 +268,7 @@ const KeyDetailsHeader = ({ const Actions = (width: number) => ( <> - {'addItems' in KEY_TYPES_ACTIONS[keyType] && ( + {KEY_TYPES_ACTIONS[keyType] && 'addItems' in KEY_TYPES_ACTIONS[keyType] && ( MIDDLE_SCREEN_RESOLUTION ? '' : KEY_TYPES_ACTIONS[keyType].addItems?.name} position="left" @@ -296,7 +298,37 @@ const KeyDetailsHeader = ({ )} - {'removeItems' in KEY_TYPES_ACTIONS[keyType] && ( + {keyType === KeyTypes.Stream && ( + MIDDLE_SCREEN_RESOLUTION ? '' : STREAM_ADD_ACTION[streamViewType].name} + position="left" + anchorClassName={cx(styles.actionBtn, { [styles.withText]: width > MIDDLE_SCREEN_RESOLUTION })} + > + <> + {width > MIDDLE_SCREEN_RESOLUTION ? ( + + {STREAM_ADD_ACTION[streamViewType].name} + + ) : ( + + )} + + + )} + {KEY_TYPES_ACTIONS[keyType] && 'removeItems' in KEY_TYPES_ACTIONS[keyType] && ( )} - {'editItem' in KEY_TYPES_ACTIONS[keyType] && ( + {KEY_TYPES_ACTIONS[keyType] && 'editItem' in KEY_TYPES_ACTIONS[keyType] && (
- {(keyType && KEY_TYPES_ACTIONS[keyType]) && Actions(width)} + {keyType && Actions(width)} { const isKeySelected = !isNull(useSelector(selectedKeyDataSelector)) const { id: instanceId } = useSelector(connectedInstanceSelector) const { viewType } = useSelector(keysSelector) + const { viewType: streamViewType } = useSelector(streamSelector) const [isAddItemPanelOpen, setIsAddItemPanelOpen] = useState(false) const [isRemoveItemPanelOpen, setIsRemoveItemPanelOpen] = useState(false) const [editItem, setEditItem] = useState(false) @@ -203,7 +206,14 @@ const KeyDetails = ({ ...props }: Props) => { )} {selectedKeyType === KeyTypes.Stream && ( - + <> + {streamViewType === StreamViewType.Streams && ( + + )} + {streamViewType === StreamViewType.Groups && ( + + )} + )}
)} diff --git a/redisinsight/ui/src/slices/browser/stream.ts b/redisinsight/ui/src/slices/browser/stream.ts index a4d84d64db..8b7340010c 100644 --- a/redisinsight/ui/src/slices/browser/stream.ts +++ b/redisinsight/ui/src/slices/browser/stream.ts @@ -14,7 +14,7 @@ import { GetStreamEntriesResponse, } from 'apiSrc/modules/browser/dto/stream.dto' import { AppDispatch, RootState } from '../store' -import { StateStream } from '../interfaces/stream' +import { StateStream, StreamViewType } from '../interfaces/stream' import { addErrorNotification, addMessageNotification } from '../app/notifications' export const initialState: StateStream = { @@ -36,6 +36,7 @@ export const initialState: StateStream = { fields: {} }, }, + viewType: StreamViewType.Groups } // A slice for recipes @@ -94,6 +95,17 @@ const streamSlice = createSlice({ state.loading = false state.error = payload }, + addNewGroup: (state) => { + state.loading = true + state.error = '' + }, + addNewGroupSuccess: (state) => { + state.loading = true + }, + addNewGroupFailure: (state, { payload }) => { + state.loading = false + state.error = payload + }, // delete Stream entries removeStreamEntries: (state) => { state.loading = true @@ -132,6 +144,9 @@ export const { addNewEntries, addNewEntriesSuccess, addNewEntriesFailure, + addNewGroup, + addNewGroupSuccess, + addNewGroupFailure, removeStreamEntries, removeStreamEntriesSuccess, removeStreamEntriesFailure, @@ -371,3 +386,39 @@ export function deleteStreamEntry(key: string, entries: string[], onSuccessActio } } } + +// Asynchronous thunk action +export function addNewGroupAction( + data: any, + onSuccess?: () => void, + onFail?: () => void +) { + return async (dispatch: AppDispatch, stateInit: () => RootState) => { + dispatch(addNewGroup()) + + try { + const state = stateInit() + const { status } = await apiService.post( + getUrl( + state.connections.instances.connectedInstance?.id, + ApiEndpoints.STREAMS_CONSUMER_GROUPS + ), + data + ) + + if (isStatusSuccessful(status)) { + dispatch(addNewGroupSuccess()) + // TODO refreshStreamGroups + //dispatch(refreshStreamEntries(data.keyName, false)) + dispatch(refreshKeyInfoAction(data.keyName)) + onSuccess?.() + } + } catch (_err) { + const error = _err as AxiosError + const errorMessage = getApiErrorMessage(error) + dispatch(addErrorNotification(error)) + dispatch(addNewGroupFailure(errorMessage)) + onFail?.() + } + } +} diff --git a/redisinsight/ui/src/slices/interfaces/stream.ts b/redisinsight/ui/src/slices/interfaces/stream.ts index 3d1935fb4d..4043583b42 100644 --- a/redisinsight/ui/src/slices/interfaces/stream.ts +++ b/redisinsight/ui/src/slices/interfaces/stream.ts @@ -6,10 +6,16 @@ type Range = { end: string, } +export enum StreamViewType { + Streams = 'Streams', + Groups = 'Groups' +} + export interface StateStream { loading: boolean error: string sortOrder: SortOrder range: Range, - data: GetStreamEntriesResponse + data: GetStreamEntriesResponse, + viewType: StreamViewType } diff --git a/redisinsight/ui/src/utils/validations.ts b/redisinsight/ui/src/utils/validations.ts index a01b03d85f..53443c4e08 100644 --- a/redisinsight/ui/src/utils/validations.ts +++ b/redisinsight/ui/src/utils/validations.ts @@ -8,10 +8,12 @@ export const MAX_REFRESH_RATE = 999.9 export const MIN_REFRESH_RATE = 1.0 export const entryIdRegex = /^(\*)$|^(([0-9]+)(-)((\*)$|([0-9]+$)))/ +export const consumerGroupIdRegex = /^(\$)$|^0$|^(([0-9]+)(-)([0-9]+$))/ export const validateField = (text: string) => text.replace(/\s/g, '') export const validateEntryId = (initValue: string) => initValue.replace(/[^0-9-*]+/gi, '') +export const validateConsumerGroupId = (initValue: string) => initValue.replace(/[^0-9-$]+/gi, '') export const validateCountNumber = (initValue: string) => { const value = initValue.replace(/[^0-9]+/gi, '') From 3ed7c48894f3fd24f3306148c41d1829448fbd94 Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Mon, 23 May 2022 14:40:39 +0400 Subject: [PATCH 02/20] #RI-2932 - Show Consumer groups --- .../page-placeholder/PagePlaceholder.tsx | 13 +- redisinsight/ui/src/constants/api.ts | 1 + .../components/hash-details/HashDetails.tsx | 2 +- .../stream-details/StreamDetails/index.ts | 3 - .../StreamDetailsWrapper.spec.tsx | 4 +- .../stream-details/StreamDetailsWrapper.tsx | 239 +++--------------- .../components/stream-details/constants.ts | 12 + .../ConsumersView/ConsumersView.spec.tsx} | 6 +- .../ConsumersView/ConsumersView.tsx | 89 +++++++ .../consumers-view/ConsumersView/index.ts | 3 + .../ConsumersView/styles.module.scss | 82 ++++++ .../ConsumersViewWrapper.spec.tsx | 12 + .../consumers-view/ConsumersViewWrapper.tsx | 201 +++++++++++++++ .../stream-details/consumers-view/index.ts | 3 + .../GroupsView/GroupsView.spec.tsx | 12 + .../groups-view/GroupsView/GroupsView.tsx | 90 +++++++ .../groups-view/GroupsView/index.ts | 3 + .../groups-view/GroupsView/styles.module.scss | 82 ++++++ .../groups-view/GroupsViewWrapper.spec.tsx | 12 + .../groups-view/GroupsViewWrapper.tsx | 205 +++++++++++++++ .../stream-details/groups-view/index.ts | 3 + .../StreamDataViewWrapper.spec.tsx | 12 + .../StreamDataViewWrapper.tsx | 228 +++++++++++++++++ .../stream-details/stream-data-view/index.ts | 3 + .../streamDataView/StreamDataView.tsx} | 13 +- .../streamDataView/StreamDetails.spec.tsx | 12 + .../stream-data-view/streamDataView/index.ts | 3 + .../streamDataView}/styles.module.scss | 0 .../stream-details/stream-tabs/StreamTabs.tsx | 36 +++ .../stream-details/stream-tabs/index.ts | 3 + redisinsight/ui/src/slices/browser/hash.ts | 3 +- redisinsight/ui/src/slices/browser/keys.ts | 26 +- redisinsight/ui/src/slices/browser/stream.ts | 239 +++++++++++++++++- .../ui/src/slices/interfaces/stream.ts | 45 +++- 34 files changed, 1451 insertions(+), 249 deletions(-) delete mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/index.ts create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/constants.ts rename redisinsight/ui/src/pages/browser/components/stream-details/{StreamDetails/StreamDetails.spec.tsx => consumers-view/ConsumersView/ConsumersView.spec.tsx} (53%) create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersView/ConsumersView.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersView/index.ts create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersView/styles.module.scss create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersViewWrapper.spec.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersViewWrapper.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/index.ts create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/GroupsView.spec.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/GroupsView.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/index.ts create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/styles.module.scss create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsViewWrapper.spec.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsViewWrapper.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/groups-view/index.ts create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.spec.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/index.ts rename redisinsight/ui/src/pages/browser/components/stream-details/{StreamDetails/StreamDetails.tsx => stream-data-view/streamDataView/StreamDataView.tsx} (96%) create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/StreamDetails.spec.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/index.ts rename redisinsight/ui/src/pages/browser/components/stream-details/{StreamDetails => stream-data-view/streamDataView}/styles.module.scss (100%) create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/index.ts diff --git a/redisinsight/ui/src/components/page-placeholder/PagePlaceholder.tsx b/redisinsight/ui/src/components/page-placeholder/PagePlaceholder.tsx index 10bcf4641f..ad3e3bb1e6 100644 --- a/redisinsight/ui/src/components/page-placeholder/PagePlaceholder.tsx +++ b/redisinsight/ui/src/components/page-placeholder/PagePlaceholder.tsx @@ -3,10 +3,15 @@ import { ReactComponent as LogoIcon } from 'uiSrc/assets/img/logo.svg' import { EuiLoadingLogo, EuiEmptyPrompt } from '@elastic/eui' const PagePlaceholder = () => ( - } - titleSize="s" - /> + <> + { process.env.NODE_ENV !== 'development' && ( + } + titleSize="s" + /> + )} + + ) export default PagePlaceholder diff --git a/redisinsight/ui/src/constants/api.ts b/redisinsight/ui/src/constants/api.ts index eef3fb81be..3d064217b9 100644 --- a/redisinsight/ui/src/constants/api.ts +++ b/redisinsight/ui/src/constants/api.ts @@ -35,6 +35,7 @@ enum ApiEndpoints { REJSON_ARRAPPEND = 'rejson-rl/arrappend', STREAMS_ENTRIES = 'streams/entries', STREAMS_ENTRIES_GET = 'streams/entries/get', + STREAMS_CONSUMER_GROUPS = 'streams/entries/get', STREAMS = 'streams', CLI = 'cli', CLI_BLOCKING_COMMANDS = 'info/cli-blocking-commands', diff --git a/redisinsight/ui/src/pages/browser/components/hash-details/HashDetails.tsx b/redisinsight/ui/src/pages/browser/components/hash-details/HashDetails.tsx index b0b0913b5e..8b1db072c5 100644 --- a/redisinsight/ui/src/pages/browser/components/hash-details/HashDetails.tsx +++ b/redisinsight/ui/src/pages/browser/components/hash-details/HashDetails.tsx @@ -159,7 +159,7 @@ const HashDetails = (props: Props) => { }) } setMatch(match) - dispatch(fetchHashFields(key, 0, SCAN_COUNT_DEFAULT, match || matchAllValue, onSuccess)) + dispatch(fetchHashFields(key, 0, SCAN_COUNT_DEFAULT, match || matchAllValue, true, onSuccess)) } } diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/index.ts b/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/index.ts deleted file mode 100644 index 7e4044b399..0000000000 --- a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import StreamDetails from './StreamDetails' - -export default StreamDetails diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetailsWrapper.spec.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetailsWrapper.spec.tsx index 93a4c23b59..efaa276284 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetailsWrapper.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetailsWrapper.spec.tsx @@ -1,12 +1,12 @@ import React from 'react' import { instance, mock } from 'ts-mockito' import { render } from 'uiSrc/utils/test-utils' -import StreamDetailsWrapper, { Props } from './StreamDetailsWrapper' +import StreamDataViewWrapper, { Props } from './StreamDetailsWrapper' const mockedProps = mock() describe('StreamDetailsWrapper', () => { it('should render', () => { - expect(render()).toBeTruthy() + expect(render()).toBeTruthy() }) }) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetailsWrapper.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetailsWrapper.tsx index 1d25b5fec8..42e059bcca 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetailsWrapper.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetailsWrapper.tsx @@ -1,227 +1,48 @@ -import { EuiText, EuiToolTip } from '@elastic/eui' -import React, { useCallback, useEffect, useState } from 'react' +import { EuiProgress, EuiTab, EuiTabs } from '@elastic/eui' +import React from 'react' import { useDispatch, useSelector } from 'react-redux' -import { keyBy } from 'lodash' -import { formatLongName } from 'uiSrc/utils' -import { streamDataSelector, deleteStreamEntry } from 'uiSrc/slices/browser/stream' -import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces' -import PopoverDelete from 'uiSrc/pages/browser/components/popover-delete/PopoverDelete' -import { getFormatTime } from 'uiSrc/utils/streamUtils' -import { KeyTypes, TableCellTextAlignment } from 'uiSrc/constants' -import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { streamSelector, setStreamViewType, streamGroupsSelector } from 'uiSrc/slices/browser/stream' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' -import { keysSelector } from 'uiSrc/slices/browser/keys' -import { StreamEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' +import { StreamViewType } from 'uiSrc/slices/interfaces/stream' -import StreamDetails from './StreamDetails' +import { streamViewTypeTabs } from './constants' +import ConsumersViewWrapper from './consumers-view' +import GroupsViewWrapper from './groups-view' +import StreamDataViewWrapper from './stream-data-view' +import StreamTabs from './stream-tabs' -import styles from './StreamDetails/styles.module.scss' - -export interface IStreamEntry extends StreamEntryDto { - editing: boolean -} - -const suffix = '_stream' -const actionsWidth = 50 -const minColumnWidth = 190 - -interface Props { +export interface Props { isFooterOpen: boolean } const StreamDetailsWrapper = (props: Props) => { - const { - entries: loadedEntries = [], - keyName: key - } = useSelector(streamDataSelector) const { id: instanceId } = useSelector(connectedInstanceSelector) - const { viewType } = useSelector(keysSelector) + const { viewType, loading } = useSelector(streamSelector) + const { loading: loadingGroups } = useSelector(streamGroupsSelector) const dispatch = useDispatch() - const [uniqFields, setUniqFields] = useState({}) - const [entries, setEntries] = useState([]) - const [columns, setColumns] = useState([]) - const [deleting, setDeleting] = useState('') - - useEffect(() => { - let fields = {} - const streamEntries: IStreamEntry[] = loadedEntries?.map((item) => { - fields = { - ...fields, - ...keyBy(Object.keys(item.fields)) - } - - return { - ...item, - editing: false, - } - }) - - setUniqFields(fields) - setEntries(streamEntries) - setColumns([idColumn, ...Object.keys(fields).map((field) => getTemplateColumn(field)), actionsColumn]) - }, [loadedEntries, deleting]) - - const closePopover = useCallback(() => { - setDeleting('') - }, []) - - const showPopover = useCallback((entry = '') => { - setDeleting(`${entry + suffix}`) - }, []) - - const onSuccessRemoved = () => { - sendEventTelemetry({ - event: getBasedOnViewTypeEvent( - viewType, - TelemetryEvent.BROWSER_KEY_VALUE_REMOVED, - TelemetryEvent.TREE_VIEW_KEY_VALUE_REMOVED - ), - eventData: { - databaseId: instanceId, - keyType: KeyTypes.Stream, - numberOfRemoved: 1, - } - }) - } - - const handleDeleteEntry = (entryId = '') => { - dispatch(deleteStreamEntry(key, [entryId], onSuccessRemoved)) - closePopover() - } - - const handleRemoveIconClick = () => { - sendEventTelemetry({ - event: getBasedOnViewTypeEvent( - viewType, - TelemetryEvent.BROWSER_KEY_VALUE_REMOVE_CLICKED, - TelemetryEvent.TREE_VIEW_KEY_VALUE_REMOVE_CLICKED - ), - eventData: { - databaseId: instanceId, - keyType: KeyTypes.Stream - } - }) - } - - const handleEditEntry = (entryId = '', editing: boolean) => { - const newFieldsState = entries.map((item) => { - if (item.id === entryId) { - return { ...item, editing } - } - return item - }) - setEntries(newFieldsState) - } - - const getTemplateColumn = (label: string) : ITableColumn => ({ - id: label, - label, - minWidth: minColumnWidth, - isSortable: false, - className: styles.cell, - headerClassName: styles.cellHeader, - headerCellClassName: 'truncateText', - render: function Id(_name: string, { id, fields }: StreamEntryDto) { - const value = fields[label] ?? '' - const cellContent = value.substring(0, 200) - const tooltipContent = formatLongName(value) - - return ( - -
- - <>{cellContent} - -
-
- ) - } - }) - - const [idColumn, actionsColumn]: ITableColumn[] = [ - { - id: 'id', - label: 'Entry ID', - absoluteWidth: minColumnWidth, - minWidth: minColumnWidth, - isSortable: true, - className: styles.cell, - headerClassName: styles.cellHeader, - render: function Id(_name: string, { id }: StreamEntryDto) { - const timestamp = id.split('-')?.[0] - return ( -
- -
- {getFormatTime(timestamp)} -
-
- -
- {id} -
-
-
- ) - }, - }, - { - id: 'actions', - label: '', - headerClassName: styles.actionsHeader, - textAlignment: TableCellTextAlignment.Left, - absoluteWidth: actionsWidth, - maxWidth: actionsWidth, - minWidth: actionsWidth, - render: function Actions(_act: any, { id }: StreamEntryDto) { - return ( -
- - Entry {id} will be removed from -
- {key} - - )} - item={id} - suffix={suffix} - deleting={deleting} - closePopover={closePopover} - updateLoading={false} - showPopover={showPopover} - testid={`remove-entry-button-${id}`} - handleDeleteItem={handleDeleteEntry} - handleButtonClick={handleRemoveIconClick} - /> -
- ) - }, - }, - ] - return ( <> - + {(loading || loadingGroups) && ( + + )} + + {viewType === StreamViewType.Data && ( + + )} + {viewType === StreamViewType.Groups && ( + + )} + {viewType === StreamViewType.Consumers && ( + + )} ) } diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/constants.ts b/redisinsight/ui/src/pages/browser/components/stream-details/constants.ts new file mode 100644 index 0000000000..f2b1b6b090 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/constants.ts @@ -0,0 +1,12 @@ +import { StreamViewType } from 'uiSrc/slices/interfaces/stream' + +export const streamViewTypeTabs = [ + { + id: StreamViewType.Data, + label: 'Stream data', + }, + { + id: StreamViewType.Groups, + label: 'Consumer Groups', + }, +] diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/StreamDetails.spec.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersView/ConsumersView.spec.tsx similarity index 53% rename from redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/StreamDetails.spec.tsx rename to redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersView/ConsumersView.spec.tsx index 46c52c999f..0f195dc526 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/StreamDetails.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersView/ConsumersView.spec.tsx @@ -1,12 +1,12 @@ import React from 'react' import { instance, mock } from 'ts-mockito' import { render } from 'uiSrc/utils/test-utils' -import StreamDetails, { Props } from './StreamDetails' +import GroupsView, { Props } from './ConsumersView' const mockedProps = mock() -describe('StreamDetails', () => { +describe('GroupsView', () => { it('should render', () => { - expect(render()).toBeTruthy() + expect(render()).toBeTruthy() }) }) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersView/ConsumersView.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersView/ConsumersView.tsx new file mode 100644 index 0000000000..105e5df1a1 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersView/ConsumersView.tsx @@ -0,0 +1,89 @@ +import React, { useState } from 'react' +import { useSelector } from 'react-redux' +import cx from 'classnames' +import { orderBy } from 'lodash' + +import { + streamGroupsSelector, +} from 'uiSrc/slices/browser/stream' +import VirtualTable from 'uiSrc/components/virtual-table/VirtualTable' +import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces' +import { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys' +import { SortOrder } from 'uiSrc/constants' +import { StreamEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' + +import styles from './styles.module.scss' + +const headerHeight = 60 +const rowHeight = 54 +const actionsWidth = 54 +const minColumnWidth = 190 +const noItemsMessageString = 'There are no Consumers in the Group.' + +interface IStreamEntry extends StreamEntryDto { + editing: boolean +} + +export interface Props { + data: IStreamEntry[] + columns: ITableColumn[] + onEditConsumer: (consumerId:string, editing: boolean) => void + onClosePopover: () => void + onSelectConsumer: ({ rowData }: { rowData: any }) => void + isFooterOpen?: boolean +} + +const ConsumersView = (props: Props) => { + const { data = [], columns = [], onClosePopover, onSelectConsumer, isFooterOpen } = props + + const { loading } = useSelector(streamGroupsSelector) + const { name: key = '' } = useSelector(selectedKeyDataSelector) ?? { } + + const [consumers, setConsumers] = useState(data) + const [sortedColumnName, setSortedColumnName] = useState('name') + const [sortedColumnOrder, setSortedColumnOrder] = useState(SortOrder.DESC) + + const onChangeSorting = (column: any, order: SortOrder) => { + setSortedColumnName(column) + setSortedColumnOrder(order) + + setConsumers(orderBy(consumers, 'name', order?.toLowerCase())) + } + + return ( + <> +
+ +
+ + ) +} + +export default ConsumersView diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersView/index.ts b/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersView/index.ts new file mode 100644 index 0000000000..171f764966 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersView/index.ts @@ -0,0 +1,3 @@ +import GroupsView from './ConsumersView' + +export default GroupsView diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersView/styles.module.scss b/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersView/styles.module.scss new file mode 100644 index 0000000000..e9e8c44e93 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersView/styles.module.scss @@ -0,0 +1,82 @@ +.container { + display: flex; + flex: 1; + width: 100%; + padding: 16px 18px; + background-color: var(--euiColorEmptyShade); + + :global { + .ReactVirtualized__Grid__innerScrollContainer { + .ReactVirtualized__Table__rowColumn { + border-right: 1px solid var(--tableDarkestBorderColor) !important; + + &:last-of-type, + &:nth-last-of-type(2) { + border-right: none !important; + } + } + + .ReactVirtualized__Table__row { + border-bottom: 1px solid var(--tableDarkestBorderColor) !important; + &:last-of-type { + border-bottom: none !important; + } + } + + & > div:hover { + background: var(--euiColorLightestShade); + + .value-table-actions { + background-color: var(--euiColorLightestShade) !important; + } + + .streamEntry { + color: var(--inputTextColor) !important; + } + + .streamEntryId { + color: var(--euiTextSubduedColor) !important; + } + } + } + + .ReactVirtualized__Table__headerRow { + border: none !important; + } + + .ReactVirtualized__Table__Grid { + border: 1px solid var(--tableDarkestBorderColor) !important; + } + } + + .cellHeader { + border: none !important; + } +} + +:global(.streamEntry) { + color: var(--euiTextSubduedColor) !important; + white-space: normal; + max-width: 100%; + word-break: break-all; +} + +:global(.streamEntryId) { + color: var(--euiColorMediumShade) !important; + display: flex; +} + +:global(.stream-entry-actions) { + margin-left: -5px; +} + +.actions, +.actionsHeader { + width: 54px; +} + +.actions { + :global(.value-table-actions) { + background-color: var(--euiColorEmptyShade) !important; + } +} diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersViewWrapper.spec.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersViewWrapper.spec.tsx new file mode 100644 index 0000000000..4e49318fb4 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersViewWrapper.spec.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import ConsumersViewWrapper, { Props } from './ConsumersViewWrapper' + +const mockedProps = mock() + +describe('ConsumersViewWrapper', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersViewWrapper.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersViewWrapper.tsx new file mode 100644 index 0000000000..d106274611 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersViewWrapper.tsx @@ -0,0 +1,201 @@ +import { EuiText } from '@elastic/eui' +import React, { useCallback, useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' + +import { + deleteStreamEntry, + fetchConsumers, + setStreamViewType, + selectedGroupSelector, + setSelectedConsumer +} from 'uiSrc/slices/browser/stream' +import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces' +import PopoverDelete from 'uiSrc/pages/browser/components/popover-delete/PopoverDelete' +import { getFormatTime } from 'uiSrc/utils/streamUtils' +import { TableCellTextAlignment } from 'uiSrc/constants' +import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { StreamEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' + +import { StreamViewType } from 'uiSrc/slices/interfaces/stream' +import ConsumersView from './ConsumersView' + +import styles from './ConsumersView/styles.module.scss' + +export interface IStreamEntry extends StreamEntryDto { + editing: boolean +} + +const suffix = '_stream_consumers' +const actionsWidth = 50 +const minColumnWidth = 190 + +interface Props { + isFooterOpen: boolean +} + +const ConsumersViewWrapper = (props: Props) => { + const { + data: loadedConsumers = [], + } = useSelector(selectedGroupSelector) ?? {} + const { id: instanceId, name: key = '' } = useSelector(connectedInstanceSelector) + + const dispatch = useDispatch() + + const [consumers, setConsumers] = useState([]) + const [deleting, setDeleting] = useState('') + + useEffect(() => { + dispatch(fetchConsumers()) + }, []) + + useEffect(() => { + const streamConsumers: IStreamEntry[] = loadedConsumers?.map((item) => ({ + ...item, + editing: false, + })) + + setConsumers(streamConsumers) + }, [loadedConsumers, deleting]) + + const closePopover = useCallback(() => { + setDeleting('') + }, []) + + const showPopover = useCallback((entry = '') => { + setDeleting(`${entry + suffix}`) + }, []) + + const handleDeleteConsumer = (entryId = '') => { + dispatch(deleteStreamEntry(key, [entryId])) + closePopover() + } + + const handleRemoveIconClick = () => { + // sendEventTelemetry({ + // event: getBasedOnViewTypeEvent( + // viewType, + // TelemetryEvent.BROWSER_KEY_VALUE_REMOVE_CLICKED, + // TelemetryEvent.TREE_VIEW_KEY_VALUE_REMOVE_CLICKED + // ), + // eventData: { + // databaseId: instanceId, + // keyType: KeyTypes.Stream + // } + // }) + } + + const handleEditConsumer = (consumerId = '', editing: boolean) => { + const newConsumersState = consumers.map((item) => { + if (item.id === consumerId) { + return { ...item, editing } + } + return item + }) + setConsumers(newConsumersState) + } + + const handleSelectConsumer = ({ rowData }: { rowData: any }) => { + dispatch(setSelectedConsumer(rowData)) + dispatch(fetchConsumers( + true, + () => dispatch(setStreamViewType(StreamViewType.Groups)) + )) + } + + const columns: ITableColumn[] = [ + + { + id: 'name', + label: 'Consumer Name', + // minWidth: 180, + truncateText: true, + isSortable: true, + // render: (cellData: ConnectionType) => + // capitalize(cellData), + }, + { + id: 'pending', + label: 'Pending', + minWidth: 106, + absoluteWidth: 106, + truncateText: true, + isSortable: true, + // render: (cellData: ConnectionType) => + // capitalize(cellData), + }, + { + id: 'time', + label: 'Idle time, ms', + absoluteWidth: 190, + minWidth: 190, + isSortable: true, + className: styles.cell, + headerClassName: styles.cellHeader, + render: function Id(_name: string, { lastDeliveredId: id }: StreamEntryDto) { + const timestamp = id?.split('-')?.[0] + return ( +
+ +
+ {getFormatTime(timestamp)} +
+
+ +
+ {id} +
+
+
+ ) + }, + }, + { + id: 'actions', + label: '', + headerClassName: styles.actionsHeader, + textAlignment: TableCellTextAlignment.Left, + absoluteWidth: actionsWidth, + maxWidth: actionsWidth, + minWidth: actionsWidth, + render: function Actions(_act: any, { id }: StreamEntryDto) { + return ( +
+ + Consumers will be removed from +
+ {key} + + )} + item={id} + suffix={suffix} + deleting={deleting} + closePopover={closePopover} + updateLoading={false} + showPopover={showPopover} + testid={`remove-consumers-button-${id}`} + handleDeleteItem={handleDeleteConsumer} + handleButtonClick={handleRemoveIconClick} + /> +
+ ) + }, + }, + ] + + return ( + <> + + + ) +} + +export default ConsumersViewWrapper diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/index.ts b/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/index.ts new file mode 100644 index 0000000000..774bd10d87 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/index.ts @@ -0,0 +1,3 @@ +import ConsumersViewWrapper from './ConsumersViewWrapper' + +export default ConsumersViewWrapper diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/GroupsView.spec.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/GroupsView.spec.tsx new file mode 100644 index 0000000000..7736c170ba --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/GroupsView.spec.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import GroupsView, { Props } from './GroupsView' + +const mockedProps = mock() + +describe('GroupsView', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/GroupsView.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/GroupsView.tsx new file mode 100644 index 0000000000..86b58d8b27 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/GroupsView.tsx @@ -0,0 +1,90 @@ +import React, { useState } from 'react' +import { useSelector } from 'react-redux' +import cx from 'classnames' +import { EuiProgress } from '@elastic/eui' +import { orderBy } from 'lodash' + +import { + streamGroupsSelector, +} from 'uiSrc/slices/browser/stream' +import VirtualTable from 'uiSrc/components/virtual-table/VirtualTable' +import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces' +import { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys' +import { SortOrder } from 'uiSrc/constants' +import { StreamEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' + +import styles from './styles.module.scss' + +const headerHeight = 60 +const rowHeight = 54 +const actionsWidth = 54 +const minColumnWidth = 190 +const noItemsMessageString = 'There are no Consumer Groups in the Stream.' + +interface IStreamEntry extends StreamEntryDto { + editing: boolean +} + +export interface Props { + data: IStreamEntry[] + columns: ITableColumn[] + onEditGroup: (groupId:string, editing: boolean) => void + onClosePopover: () => void + onSelectGroup: ({ rowData }: { rowData: any }) => void + isFooterOpen?: boolean +} + +const ConsumerGroups = (props: Props) => { + const { data = [], columns = [], onClosePopover, onSelectGroup, isFooterOpen } = props + + const { loading } = useSelector(streamGroupsSelector) + const { name: key = '' } = useSelector(selectedKeyDataSelector) ?? { } + + const [groups, setGroups] = useState(data) + const [sortedColumnName, setSortedColumnName] = useState('name') + const [sortedColumnOrder, setSortedColumnOrder] = useState(SortOrder.DESC) + + const onChangeSorting = (column: any, order: SortOrder) => { + setSortedColumnName(column) + setSortedColumnOrder(order) + + setGroups(orderBy(groups, 'name', order?.toLowerCase())) + } + + return ( + <> +
+ +
+ + ) +} + +export default ConsumerGroups diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/index.ts b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/index.ts new file mode 100644 index 0000000000..559715f535 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/index.ts @@ -0,0 +1,3 @@ +import GroupsView from './GroupsView' + +export default GroupsView diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/styles.module.scss b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/styles.module.scss new file mode 100644 index 0000000000..e9e8c44e93 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/styles.module.scss @@ -0,0 +1,82 @@ +.container { + display: flex; + flex: 1; + width: 100%; + padding: 16px 18px; + background-color: var(--euiColorEmptyShade); + + :global { + .ReactVirtualized__Grid__innerScrollContainer { + .ReactVirtualized__Table__rowColumn { + border-right: 1px solid var(--tableDarkestBorderColor) !important; + + &:last-of-type, + &:nth-last-of-type(2) { + border-right: none !important; + } + } + + .ReactVirtualized__Table__row { + border-bottom: 1px solid var(--tableDarkestBorderColor) !important; + &:last-of-type { + border-bottom: none !important; + } + } + + & > div:hover { + background: var(--euiColorLightestShade); + + .value-table-actions { + background-color: var(--euiColorLightestShade) !important; + } + + .streamEntry { + color: var(--inputTextColor) !important; + } + + .streamEntryId { + color: var(--euiTextSubduedColor) !important; + } + } + } + + .ReactVirtualized__Table__headerRow { + border: none !important; + } + + .ReactVirtualized__Table__Grid { + border: 1px solid var(--tableDarkestBorderColor) !important; + } + } + + .cellHeader { + border: none !important; + } +} + +:global(.streamEntry) { + color: var(--euiTextSubduedColor) !important; + white-space: normal; + max-width: 100%; + word-break: break-all; +} + +:global(.streamEntryId) { + color: var(--euiColorMediumShade) !important; + display: flex; +} + +:global(.stream-entry-actions) { + margin-left: -5px; +} + +.actions, +.actionsHeader { + width: 54px; +} + +.actions { + :global(.value-table-actions) { + background-color: var(--euiColorEmptyShade) !important; + } +} diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsViewWrapper.spec.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsViewWrapper.spec.tsx new file mode 100644 index 0000000000..0e2908c5b8 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsViewWrapper.spec.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import GroupsViewWrapper, { Props } from './GroupsViewWrapper' + +const mockedProps = mock() + +describe('GroupsViewWrapper', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsViewWrapper.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsViewWrapper.tsx new file mode 100644 index 0000000000..d86a4c1acb --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsViewWrapper.tsx @@ -0,0 +1,205 @@ +import { EuiText } from '@elastic/eui' +import React, { useCallback, useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' + +import { streamGroupsSelector, deleteStreamEntry, fetchConsumerGroups, setSelectedGroup, fetchConsumers, setStreamViewType } from 'uiSrc/slices/browser/stream' +import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces' +import PopoverDelete from 'uiSrc/pages/browser/components/popover-delete/PopoverDelete' +import { getFormatTime } from 'uiSrc/utils/streamUtils' +import { TableCellTextAlignment } from 'uiSrc/constants' +import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { StreamEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' + +import { StreamViewType } from 'uiSrc/slices/interfaces/stream' +import GroupsView from './GroupsView' + +import styles from './GroupsView/styles.module.scss' + +export interface IStreamEntry extends StreamEntryDto { + editing: boolean +} + +const suffix = '_stream_groups' +const actionsWidth = 50 +const minColumnWidth = 190 + +interface Props { + isFooterOpen: boolean +} + +const GroupsViewWrapper = (props: Props) => { + const { + data: loadedGroups = [], + } = useSelector(streamGroupsSelector) + const { id: instanceId, name: key = '' } = useSelector(connectedInstanceSelector) + + const dispatch = useDispatch() + + const [groups, setGroups] = useState([]) + const [deleting, setDeleting] = useState('') + + useEffect(() => { + dispatch(fetchConsumerGroups()) + }, []) + + useEffect(() => { + const streamGroups: IStreamEntry[] = loadedGroups?.map((item) => ({ + ...item, + editing: false, + })) + + setGroups(streamGroups) + }, [loadedGroups, deleting]) + + const closePopover = useCallback(() => { + setDeleting('') + }, []) + + const showPopover = useCallback((entry = '') => { + setDeleting(`${entry + suffix}`) + }, []) + + const handleDeleteGroup = (entryId = '') => { + dispatch(deleteStreamEntry(key, [entryId])) + closePopover() + } + + const handleRemoveIconClick = () => { + // sendEventTelemetry({ + // event: getBasedOnViewTypeEvent( + // viewType, + // TelemetryEvent.BROWSER_KEY_VALUE_REMOVE_CLICKED, + // TelemetryEvent.TREE_VIEW_KEY_VALUE_REMOVE_CLICKED + // ), + // eventData: { + // databaseId: instanceId, + // keyType: KeyTypes.Stream + // } + // }) + } + + const handleEditGroup = (groupId = '', editing: boolean) => { + const newGroupsState = groups.map((item) => { + if (item.id === groupId) { + return { ...item, editing } + } + return item + }) + setGroups(newGroupsState) + } + + const handleSelectGroup = ({ rowData }: { rowData: any }) => { + dispatch(setSelectedGroup(rowData)) + dispatch(fetchConsumers( + true, + () => dispatch(setStreamViewType(StreamViewType.Consumers)) + )) + } + + const columns: ITableColumn[] = [ + + { + id: 'name', + label: 'Group Name', + // minWidth: 180, + truncateText: true, + isSortable: true, + // render: (cellData: ConnectionType) => + // capitalize(cellData), + }, + { + id: 'consumers', + label: 'Consumers', + minWidth: 130, + absoluteWidth: 130, + truncateText: true, + isSortable: true, + // render: (cellData: ConnectionType) => + // capitalize(cellData), + }, + { + id: 'pending', + label: 'Pending', + minWidth: 106, + absoluteWidth: 106, + truncateText: true, + isSortable: true, + // render: (cellData: ConnectionType) => + // capitalize(cellData), + }, + { + id: 'lastDeliveredId', + label: 'Last Delivered ID', + absoluteWidth: 190, + minWidth: 190, + isSortable: true, + className: styles.cell, + headerClassName: styles.cellHeader, + render: function Id(_name: string, { lastDeliveredId: id }: StreamEntryDto) { + const timestamp = id?.split('-')?.[0] + return ( +
+ +
+ {getFormatTime(timestamp)} +
+
+ +
+ {id} +
+
+
+ ) + }, + }, + { + id: 'actions', + label: '', + headerClassName: styles.actionsHeader, + textAlignment: TableCellTextAlignment.Left, + absoluteWidth: actionsWidth, + maxWidth: actionsWidth, + minWidth: actionsWidth, + render: function Actions(_act: any, { id }: StreamEntryDto) { + return ( +
+ + Groups will be removed from +
+ {key} + + )} + item={id} + suffix={suffix} + deleting={deleting} + closePopover={closePopover} + updateLoading={false} + showPopover={showPopover} + testid={`remove-groups-button-${id}`} + handleDeleteItem={handleDeleteGroup} + handleButtonClick={handleRemoveIconClick} + /> +
+ ) + }, + }, + ] + + return ( + <> + + + ) +} + +export default GroupsViewWrapper diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/index.ts b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/index.ts new file mode 100644 index 0000000000..2156925913 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/index.ts @@ -0,0 +1,3 @@ +import GroupsViewWrapper from './GroupsViewWrapper' + +export default GroupsViewWrapper diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.spec.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.spec.tsx new file mode 100644 index 0000000000..5c8a4cfa0c --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.spec.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import StreamDataViewWrapper, { Props } from './StreamDataViewWrapper' + +const mockedProps = mock() + +describe('StreamDataViewWrapper', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx new file mode 100644 index 0000000000..32ac5dffaa --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx @@ -0,0 +1,228 @@ +import { EuiText, EuiToolTip } from '@elastic/eui' +import React, { useCallback, useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { keyBy } from 'lodash' + +import { formatLongName } from 'uiSrc/utils' +import { streamDataSelector, deleteStreamEntry } from 'uiSrc/slices/browser/stream' +import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces' +import PopoverDelete from 'uiSrc/pages/browser/components/popover-delete/PopoverDelete' +import { getFormatTime } from 'uiSrc/utils/streamUtils' +import { KeyTypes, TableCellTextAlignment } from 'uiSrc/constants' +import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { keysSelector } from 'uiSrc/slices/browser/keys' +import { StreamEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' + +import StreamDataView from './StreamDataView' +import styles from './StreamDataView/styles.module.scss' + +export interface IStreamEntry extends StreamEntryDto { + editing: boolean +} + +const suffix = '_stream' +const actionsWidth = 50 +const minColumnWidth = 190 + +export interface Props { + isFooterOpen: boolean +} + +const StreamDataViewWrapper = (props: Props) => { + const { + entries: loadedEntries = [], + keyName: key + } = useSelector(streamDataSelector) + const { id: instanceId } = useSelector(connectedInstanceSelector) + const { viewType: browserViewType } = useSelector(keysSelector) + + const dispatch = useDispatch() + + const [uniqFields, setUniqFields] = useState({}) + const [entries, setEntries] = useState([]) + const [columns, setColumns] = useState([]) + const [deleting, setDeleting] = useState('') + + useEffect(() => { + let fields = {} + const streamEntries: IStreamEntry[] = loadedEntries?.map((item) => { + fields = { + ...fields, + ...keyBy(Object.keys(item.fields)) + } + + return { + ...item, + editing: false, + } + }) + + setUniqFields(fields) + setEntries(streamEntries) + setColumns([idColumn, ...Object.keys(fields).map((field) => getTemplateColumn(field)), actionsColumn]) + }, [loadedEntries, deleting]) + + const closePopover = useCallback(() => { + setDeleting('') + }, []) + + const showPopover = useCallback((entry = '') => { + setDeleting(`${entry + suffix}`) + }, []) + + const onSuccessRemoved = () => { + sendEventTelemetry({ + event: getBasedOnViewTypeEvent( + browserViewType, + TelemetryEvent.BROWSER_KEY_VALUE_REMOVED, + TelemetryEvent.TREE_VIEW_KEY_VALUE_REMOVED + ), + eventData: { + databaseId: instanceId, + keyType: KeyTypes.Stream, + numberOfRemoved: 1, + } + }) + } + + const handleDeleteEntry = (entryId = '') => { + dispatch(deleteStreamEntry(key, [entryId], onSuccessRemoved)) + closePopover() + } + + const handleRemoveIconClick = () => { + sendEventTelemetry({ + event: getBasedOnViewTypeEvent( + browserViewType, + TelemetryEvent.BROWSER_KEY_VALUE_REMOVE_CLICKED, + TelemetryEvent.TREE_VIEW_KEY_VALUE_REMOVE_CLICKED + ), + eventData: { + databaseId: instanceId, + keyType: KeyTypes.Stream + } + }) + } + + const handleEditEntry = (entryId = '', editing: boolean) => { + const newFieldsState = entries.map((item) => { + if (item.id === entryId) { + return { ...item, editing } + } + return item + }) + setEntries(newFieldsState) + } + + const getTemplateColumn = (label: string) : ITableColumn => ({ + id: label, + label, + minWidth: minColumnWidth, + isSortable: false, + className: styles.cell, + headerClassName: styles.cellHeader, + headerCellClassName: 'truncateText', + render: function Id(_name: string, { id, fields }: StreamEntryDto) { + const value = fields[label] ?? '' + const cellContent = value.substring(0, 200) + const tooltipContent = formatLongName(value) + + return ( + +
+ + <>{cellContent} + +
+
+ ) + } + }) + + const [idColumn, actionsColumn]: ITableColumn[] = [ + { + id: 'id', + label: 'Entry ID', + absoluteWidth: minColumnWidth, + minWidth: minColumnWidth, + isSortable: true, + className: styles.cell, + headerClassName: styles.cellHeader, + render: function Id(_name: string, { id }: StreamEntryDto) { + const timestamp = id.split('-')?.[0] + return ( +
+ +
+ {getFormatTime(timestamp)} +
+
+ +
+ {id} +
+
+
+ ) + }, + }, + { + id: 'actions', + label: '', + headerClassName: styles.actionsHeader, + textAlignment: TableCellTextAlignment.Left, + absoluteWidth: actionsWidth, + maxWidth: actionsWidth, + minWidth: actionsWidth, + render: function Actions(_act: any, { id }: StreamEntryDto) { + return ( +
+ + Entry {id} will be removed from +
+ {key} + + )} + item={id} + suffix={suffix} + deleting={deleting} + closePopover={closePopover} + updateLoading={false} + showPopover={showPopover} + testid={`remove-entry-button-${id}`} + handleDeleteItem={handleDeleteEntry} + handleButtonClick={handleRemoveIconClick} + /> +
+ ) + }, + }, + ] + + return ( + <> + + + ) +} + +export default StreamDataViewWrapper diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/index.ts b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/index.ts new file mode 100644 index 0000000000..c962efae0b --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/index.ts @@ -0,0 +1,3 @@ +import StreamDataViewWrapper from './StreamDataViewWrapper' + +export default StreamDataViewWrapper diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/StreamDetails.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/StreamDataView.tsx similarity index 96% rename from redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/StreamDetails.tsx rename to redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/StreamDataView.tsx index 5171aa482b..8d0b443cff 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/StreamDetails.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/StreamDataView.tsx @@ -2,7 +2,6 @@ import React, { useCallback, useMemo, useState, useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' import { last, isNull } from 'lodash' import cx from 'classnames' -import { EuiButtonIcon, EuiProgress } from '@elastic/eui' import { fetchMoreStreamEntries, @@ -57,7 +56,7 @@ export interface Props { isFooterOpen?: boolean } -const StreamDetails = (props: Props) => { +const StreamDataView = (props: Props) => { const { data: entries = [], columns = [], onClosePopover, isFooterOpen } = props const dispatch = useDispatch() @@ -207,14 +206,6 @@ const StreamDetails = (props: Props) => { return ( <> - {loading && ( - - )} {shouldFilterRender ? ( { ) } -export default StreamDetails +export default StreamDataView diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/StreamDetails.spec.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/StreamDetails.spec.tsx new file mode 100644 index 0000000000..0ef6e2105c --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/StreamDetails.spec.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import StreamDataView, { Props } from './StreamDataView' + +const mockedProps = mock() + +describe('StreamDataView', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/index.ts b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/index.ts new file mode 100644 index 0000000000..bc8c479afd --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/index.ts @@ -0,0 +1,3 @@ +import StreamDataView from './StreamDataView' + +export default StreamDataView diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/styles.module.scss b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/styles.module.scss rename to redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/styles.module.scss diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.tsx new file mode 100644 index 0000000000..dd450b0503 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.tsx @@ -0,0 +1,36 @@ +import { EuiTab, EuiTabs } from '@elastic/eui' +import React from 'react' +import { useDispatch, useSelector } from 'react-redux' + +import { streamSelector, setStreamViewType } from 'uiSrc/slices/browser/stream' +import { StreamViewType } from 'uiSrc/slices/interfaces/stream' + +import { streamViewTypeTabs } from '../constants' + +const StreamTabs = () => { + const { viewType } = useSelector(streamSelector) + + const dispatch = useDispatch() + + const onSelectedTabChanged = (id: StreamViewType) => { + dispatch(setStreamViewType(id)) + } + + const renderTabs = () => + streamViewTypeTabs.map(({ id, label }, i) => ( + onSelectedTabChanged(id)} + // eslint-disable-next-line react/no-array-index-key + key={i} + > + {label} + + )) + + return ( + {renderTabs()} + ) +} + +export default StreamTabs diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/index.ts b/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/index.ts new file mode 100644 index 0000000000..0f966019fd --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/index.ts @@ -0,0 +1,3 @@ +import StreamTabs from './StreamTabs' + +export default StreamTabs diff --git a/redisinsight/ui/src/slices/browser/hash.ts b/redisinsight/ui/src/slices/browser/hash.ts index 3f767acd87..8eb4515856 100644 --- a/redisinsight/ui/src/slices/browser/hash.ts +++ b/redisinsight/ui/src/slices/browser/hash.ts @@ -194,10 +194,11 @@ export function fetchHashFields( cursor: number, count: number, match: string, + resetData: boolean = true, onSuccess?: (data: GetHashFieldsResponse) => void, ) { return async (dispatch: AppDispatch, stateInit: () => RootState) => { - dispatch(loadHashFields([isNull(match) ? '*' : match, true])) + dispatch(loadHashFields([isNull(match) ? '*' : match, resetData])) try { const state = stateInit() diff --git a/redisinsight/ui/src/slices/browser/keys.ts b/redisinsight/ui/src/slices/browser/keys.ts index bcd89455ed..924972804a 100644 --- a/redisinsight/ui/src/slices/browser/keys.ts +++ b/redisinsight/ui/src/slices/browser/keys.ts @@ -30,10 +30,11 @@ import { fetchSetMembers } from './set' import { fetchReJSON } from './rejson' import { setHashInitialState, fetchHashFields } from './hash' import { setListInitialState, fetchListElements } from './list' -import { fetchStreamEntries } from './stream' +import { fetchConsumerGroups, fetchStreamEntries } from './stream' import { addErrorNotification, addMessageNotification } from '../app/notifications' import { KeysStore, KeyViewType } from '../interfaces/keys' import { AppDispatch, RootState } from '../store' +import { StreamViewType } from '../interfaces/stream' export const initialState: KeysStore = { loading: false, @@ -545,12 +546,23 @@ export function fetchKeyInfo(key: string, resetData?: boolean) { dispatch(fetchReJSON(key, '.', resetData)) } if (data.type === KeyTypes.Stream) { - dispatch(fetchStreamEntries( - key, - SCAN_COUNT_DEFAULT, - SortOrder.DESC, - resetData - )) + const { viewType } = state.browser.stream + + if (viewType === StreamViewType.Data) { + dispatch(fetchStreamEntries( + key, + SCAN_COUNT_DEFAULT, + SortOrder.DESC, + resetData + )) + } else if (viewType === StreamViewType.Groups) { + dispatch(fetchConsumerGroups( + // key, + // SCAN_COUNT_DEFAULT, + // SortOrder.DESC, + // resetData + )) + } } } catch (_err) { const error = _err as AxiosError diff --git a/redisinsight/ui/src/slices/browser/stream.ts b/redisinsight/ui/src/slices/browser/stream.ts index a4d84d64db..3cc1d536ae 100644 --- a/redisinsight/ui/src/slices/browser/stream.ts +++ b/redisinsight/ui/src/slices/browser/stream.ts @@ -12,9 +12,10 @@ import { AddStreamEntriesDto, AddStreamEntriesResponse, GetStreamEntriesResponse, + StreamEntryDto, } from 'apiSrc/modules/browser/dto/stream.dto' import { AppDispatch, RootState } from '../store' -import { StateStream } from '../interfaces/stream' +import { StateStream, StreamViewType } from '../interfaces/stream' import { addErrorNotification, addMessageNotification } from '../app/notifications' export const initialState: StateStream = { @@ -22,6 +23,8 @@ export const initialState: StateStream = { error: '', sortOrder: SortOrder.DESC, range: { start: '', end: '' }, + viewType: StreamViewType.Data, + // viewType: StreamViewType.Groups, data: { total: 0, entries: [], @@ -36,6 +39,12 @@ export const initialState: StateStream = { fields: {} }, }, + groups: { + loading: false, + error: '', + data: [], + selectedGroup: null, + } } // A slice for recipes @@ -118,6 +127,67 @@ const streamSlice = createSlice({ end: '', } }, + + setStreamViewType: (state, { payload }: PayloadAction) => { + state.viewType = payload + }, + + // load stream consumer groups entries + loadConsumerGroups: (state, { payload: resetData = true }: PayloadAction>) => { + state.groups.loading = true + state.groups.error = '' + + if (resetData) { + state.groups.data = initialState.groups.data + } + }, + loadConsumerGroupsSuccess: (state, { payload }: PayloadAction) => { + state.groups.loading = false + state.groups.data = payload + }, + loadConsumerGroupsFailure: (state, { payload }) => { + state.groups.loading = false + state.groups.error = payload + }, + setSelectedGroup: (state, { payload }) => { + state.groups.selectedGroup = payload + }, + + setSelectedConsumer: (state, { payload }) => { + state.groups.selectedGroup = { + ...state.groups.selectedGroup, + selectedConsumer: payload + } + }, + + loadConsumersSuccess: (state, { payload }: PayloadAction) => { + state.groups.loading = false + + state.groups.selectedGroup = { + ...state.groups.selectedGroup, + data: payload + } + }, + + loadConsumerMessagesSuccess: (state, { payload }: PayloadAction) => { + state.groups.loading = false + + state.groups.selectedGroup = { + ...state.groups.selectedGroup, + selectedConsumer: { + ...state.groups.selectedGroup.selectedConsumer, + data: payload + } + } + }, + + setConsumerGroupsSortOrder: (state, { payload }: PayloadAction) => { + state.groups.sortOrder = payload + }, + loadMoreConsumerGroupsFailure: (state, { payload }) => { + state.loading = false + state.error = payload + }, }, }) @@ -137,13 +207,26 @@ export const { removeStreamEntriesFailure, updateStart, updateEnd, - cleanRangeFilter + cleanRangeFilter, + setStreamViewType, + loadConsumerGroups, + loadConsumerGroupsSuccess, + loadConsumerGroupsFailure, + loadConsumersSuccess, + loadConsumerMessagesSuccess, + setSelectedGroup, + setSelectedConsumer, } = streamSlice.actions // A selector export const streamSelector = (state: RootState) => state.browser.stream export const streamDataSelector = (state: RootState) => state.browser.stream?.data export const streamRangeSelector = (state: RootState) => state.browser.stream?.range +export const streamGroupsSelector = (state: RootState) => state.browser.stream?.groups +export const streamGroupsDataSelector = (state: RootState) => state.browser.stream?.groups?.data || [] +export const selectedGroupSelector = (state: RootState) => state.browser.stream?.groups?.selectedGroup +export const selectedConsumerSelector = (state: RootState) => + state.browser.stream?.groups?.selectedGroup?.selectedConsumer || {} // The reducer export default streamSlice.reducer @@ -303,7 +386,7 @@ export function fetchMoreStreamEntries( export function addNewEntriesAction( data: AddStreamEntriesDto, onSuccess?: () => void, - onFail?: () => void + onFailed?: () => void ) { return async (dispatch: AppDispatch, stateInit: () => RootState) => { dispatch(addNewEntries()) @@ -329,7 +412,7 @@ export function addNewEntriesAction( const errorMessage = getApiErrorMessage(error) dispatch(addErrorNotification(error)) dispatch(addNewEntriesFailure(errorMessage)) - onFail?.() + onFailed?.() } } } @@ -371,3 +454,151 @@ export function deleteStreamEntry(key: string, entries: string[], onSuccessActio } } } + +// Asynchronous thunk action +export function fetchConsumerGroups( + resetData?: boolean, + onSuccess?: (data: GetStreamEntriesResponse) => void, + onFailed?: () => void, +) { + return async (dispatch: AppDispatch, stateInit: () => RootState) => { + dispatch(loadConsumerGroups(resetData)) + + try { + const state = stateInit() + const { status } = await apiService.post( + getUrl( + state.app.context.contextInstanceId, + ApiEndpoints.STREAMS_CONSUMER_GROUPS + ), + { + keyName: state.app.context.browser.keyList.selectedKey, + count: 500, + sortOrder: SortOrder.ASC + }, + ) + + const data = [] + + for (let i = 0; i < 1000; i++) { + data.push({ + name: `group ${i}`, + consumers: Math.floor(Math.random() * 10), + pending: Math.floor(Math.random() * 10), + lastDeliveredId: `${Date.now()}-0` + },) + } + + if (isStatusSuccessful(status)) { + dispatch(loadConsumerGroupsSuccess(data)) + onSuccess?.(data) + } + } catch (_err) { + if (!axios.isCancel(_err)) { + const error = _err as AxiosError + const errorMessage = getApiErrorMessage(error) + dispatch(addErrorNotification(error)) + dispatch(loadConsumerGroupsFailure(errorMessage)) + onFailed?.() + } + } + } +} + +// Asynchronous thunk action +export function fetchConsumers( + resetData?: boolean, + onSuccess?: (data: GetStreamEntriesResponse) => void, + onFailed?: () => void, +) { + return async (dispatch: AppDispatch, stateInit: () => RootState) => { + dispatch(loadConsumerGroups(resetData)) + + try { + const state = stateInit() + const { status } = await apiService.post( + getUrl( + state.app.context.contextInstanceId, + ApiEndpoints.STREAMS_CONSUMER_GROUPS + ), + { + keyName: state.app.context.browser.keyList.selectedKey, + count: 500, + sortOrder: SortOrder.ASC + }, + ) + + const data = [] + + for (let i = 0; i < 1000; i++) { + data.push({ + name: `consumer ${i}`, + pending: Math.floor(Math.random() * 10), + time: Date.now() + }) + } + + if (isStatusSuccessful(status)) { + dispatch(loadConsumersSuccess(data)) + onSuccess?.(data) + } + } catch (_err) { + if (!axios.isCancel(_err)) { + const error = _err as AxiosError + const errorMessage = getApiErrorMessage(error) + dispatch(addErrorNotification(error)) + dispatch(loadConsumerGroupsFailure(errorMessage)) + onFailed?.() + } + } + } +} + +// Asynchronous thunk action +export function fetchConsumerMessages( + resetData?: boolean, + onSuccess?: (data: GetStreamEntriesResponse) => void, + onFailed?: () => void, +) { + return async (dispatch: AppDispatch, stateInit: () => RootState) => { + dispatch(loadConsumerGroups(resetData)) + + try { + const state = stateInit() + const { status } = await apiService.post( + getUrl( + state.app.context.contextInstanceId, + ApiEndpoints.STREAMS_CONSUMER_GROUPS + ), + { + keyName: state.app.context.browser.keyList.selectedKey, + count: 500, + sortOrder: SortOrder.ASC + }, + ) + + const data = [] + + for (let i = 0; i < 1000; i++) { + data.push({ + entryId: `${Date.now()}-0`, + lastMessage: Date.now(), + timesDelivered: Math.floor(Math.random() * 10), + }) + } + + if (isStatusSuccessful(status)) { + dispatch(loadConsumerMessagesSuccess(data)) + onSuccess?.(data) + } + } catch (_err) { + if (!axios.isCancel(_err)) { + const error = _err as AxiosError + const errorMessage = getApiErrorMessage(error) + dispatch(addErrorNotification(error)) + dispatch(loadConsumerGroupsFailure(errorMessage)) + onFailed?.() + } + } + } +} diff --git a/redisinsight/ui/src/slices/interfaces/stream.ts b/redisinsight/ui/src/slices/interfaces/stream.ts index 3d1935fb4d..e11f94afff 100644 --- a/redisinsight/ui/src/slices/interfaces/stream.ts +++ b/redisinsight/ui/src/slices/interfaces/stream.ts @@ -1,15 +1,52 @@ -import { GetStreamEntriesResponse } from 'apiSrc/modules/browser/dto/stream.dto' +import { GetStreamEntriesResponse, StreamEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' import { SortOrder } from 'uiSrc/constants' +import { Nullable } from 'uiSrc/utils' type Range = { - start: string, - end: string, + start: string + end: string +} + +export enum StreamViewType { + Data = 'Data', + Groups = 'Groups', + Consumers = 'Consumers', + Messages = 'Messages', } export interface StateStream { loading: boolean error: string sortOrder: SortOrder - range: Range, + range: Range data: GetStreamEntriesResponse + viewType: StreamViewType + groups: StateConsumerGroups +} + +export interface StateConsumerGroups { + loading: boolean + error: string + data: StreamEntryDto[] + selectedGroup: Nullable +} + +export interface StateSelectedGroup { + loading: boolean + name: string + data: StreamEntryDto[] + selectedConsumer: Nullable +} + +export interface StateSelectedConsumer { + loading: boolean + name: string + data: StreamEntryDto[] + selectedMsg: Nullable +} + +export interface StateSelectedMessage { + loading: boolean + name: string + data: StreamEntryDto[] } From 1d18c3b314fb8c91a17887101ffa7486383ae481 Mon Sep 17 00:00:00 2001 From: "Roman.Sergeenko" Date: Mon, 23 May 2022 15:28:53 +0400 Subject: [PATCH 03/20] #RI-810 - integration with consumer groups list --- redisinsight/ui/src/constants/keys.ts | 14 +++++++++++++- .../key-details/KeyDetails/KeyDetails.tsx | 6 +++--- redisinsight/ui/src/slices/browser/stream.ts | 3 +-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/redisinsight/ui/src/constants/keys.ts b/redisinsight/ui/src/constants/keys.ts index 15c549f045..1f9a927a45 100644 --- a/redisinsight/ui/src/constants/keys.ts +++ b/redisinsight/ui/src/constants/keys.ts @@ -118,12 +118,24 @@ export const KEY_TYPES_ACTIONS: KeyTypesActions = Object.freeze({ [KeyTypes.ReJSON]: {} }) +export const STREAM_ADD_GROUP_VIEW_TYPES = [ + StreamViewType.Groups, + StreamViewType.Consumers, + StreamViewType.Messages +] + export const STREAM_ADD_ACTION = Object.freeze({ - [StreamViewType.Streams]: { + [StreamViewType.Data]: { name: 'New Entry' }, [StreamViewType.Groups]: { name: 'New Group' + }, + [StreamViewType.Consumers]: { + name: 'New Group' + }, + [StreamViewType.Messages]: { + name: 'New Group' } }) diff --git a/redisinsight/ui/src/pages/browser/components/key-details/KeyDetails/KeyDetails.tsx b/redisinsight/ui/src/pages/browser/components/key-details/KeyDetails/KeyDetails.tsx index 836b68c1fd..9bd89e4fa7 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details/KeyDetails/KeyDetails.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-details/KeyDetails/KeyDetails.tsx @@ -23,7 +23,7 @@ import { keysSelector, } from 'uiSrc/slices/browser/keys' import { cleanRangeFilter, streamSelector } from 'uiSrc/slices/browser/stream' -import { KeyTypes, ModulesKeyTypes, MODULES_KEY_TYPES_NAMES } from 'uiSrc/constants' +import { KeyTypes, ModulesKeyTypes, MODULES_KEY_TYPES_NAMES, STREAM_ADD_GROUP_VIEW_TYPES } from 'uiSrc/constants' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { StreamViewType } from 'uiSrc/slices/interfaces/stream' import { sendEventTelemetry, TelemetryEvent, getBasedOnViewTypeEvent } from 'uiSrc/telemetry' @@ -207,10 +207,10 @@ const KeyDetails = ({ ...props }: Props) => { )} {selectedKeyType === KeyTypes.Stream && ( <> - {streamViewType === StreamViewType.Streams && ( + {streamViewType === StreamViewType.Data && ( )} - {streamViewType === StreamViewType.Groups && ( + {STREAM_ADD_GROUP_VIEW_TYPES.includes(streamViewType) && ( )} diff --git a/redisinsight/ui/src/slices/browser/stream.ts b/redisinsight/ui/src/slices/browser/stream.ts index b08f27fe96..076530019a 100644 --- a/redisinsight/ui/src/slices/browser/stream.ts +++ b/redisinsight/ui/src/slices/browser/stream.ts @@ -490,8 +490,7 @@ export function addNewGroupAction( if (isStatusSuccessful(status)) { dispatch(addNewGroupSuccess()) - // TODO refreshStreamGroups - //dispatch(refreshStreamEntries(data.keyName, false)) + dispatch(fetchConsumerGroups(false)) dispatch(refreshKeyInfoAction(data.keyName)) onSuccess?.() } From a37501bd3865d7be572ae2ffc73e58298b538e08 Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Wed, 25 May 2022 16:20:29 +0400 Subject: [PATCH 04/20] added fetched consumer groups list and consumers per group --- redisinsight/ui/src/constants/api.ts | 4 +- redisinsight/ui/src/slices/browser/stream.ts | 42 +++++-------------- .../ui/src/slices/interfaces/stream.ts | 8 +++- 3 files changed, 19 insertions(+), 35 deletions(-) diff --git a/redisinsight/ui/src/constants/api.ts b/redisinsight/ui/src/constants/api.ts index 3d064217b9..a903cbc127 100644 --- a/redisinsight/ui/src/constants/api.ts +++ b/redisinsight/ui/src/constants/api.ts @@ -35,7 +35,9 @@ enum ApiEndpoints { REJSON_ARRAPPEND = 'rejson-rl/arrappend', STREAMS_ENTRIES = 'streams/entries', STREAMS_ENTRIES_GET = 'streams/entries/get', - STREAMS_CONSUMER_GROUPS = 'streams/entries/get', + STREAMS_CONSUMER_GROUPS = 'streams/consumer-groups', + STREAMS_CONSUMER_GROUPS_GET = 'streams/consumer-groups/get', + STREAMS_CONSUMERS_GET = 'streams/consumer-groups/consumers/get', STREAMS = 'streams', CLI = 'cli', CLI_BLOCKING_COMMANDS = 'info/cli-blocking-commands', diff --git a/redisinsight/ui/src/slices/browser/stream.ts b/redisinsight/ui/src/slices/browser/stream.ts index 3cc1d536ae..42f7634fe5 100644 --- a/redisinsight/ui/src/slices/browser/stream.ts +++ b/redisinsight/ui/src/slices/browser/stream.ts @@ -11,6 +11,8 @@ import successMessages from 'uiSrc/components/notifications/success-messages' import { AddStreamEntriesDto, AddStreamEntriesResponse, + ConsumerDto, + ConsumerGroupDto, GetStreamEntriesResponse, StreamEntryDto, } from 'apiSrc/modules/browser/dto/stream.dto' @@ -141,7 +143,7 @@ const streamSlice = createSlice({ state.groups.data = initialState.groups.data } }, - loadConsumerGroupsSuccess: (state, { payload }: PayloadAction) => { + loadConsumerGroupsSuccess: (state, { payload }: PayloadAction) => { state.groups.loading = false state.groups.data = payload }, @@ -160,7 +162,7 @@ const streamSlice = createSlice({ } }, - loadConsumersSuccess: (state, { payload }: PayloadAction) => { + loadConsumersSuccess: (state, { payload }: PayloadAction) => { state.groups.loading = false state.groups.selectedGroup = { @@ -466,29 +468,16 @@ export function fetchConsumerGroups( try { const state = stateInit() - const { status } = await apiService.post( + const { data, status } = await apiService.post( getUrl( state.app.context.contextInstanceId, - ApiEndpoints.STREAMS_CONSUMER_GROUPS + ApiEndpoints.STREAMS_CONSUMER_GROUPS_GET ), { keyName: state.app.context.browser.keyList.selectedKey, - count: 500, - sortOrder: SortOrder.ASC }, ) - const data = [] - - for (let i = 0; i < 1000; i++) { - data.push({ - name: `group ${i}`, - consumers: Math.floor(Math.random() * 10), - pending: Math.floor(Math.random() * 10), - lastDeliveredId: `${Date.now()}-0` - },) - } - if (isStatusSuccessful(status)) { dispatch(loadConsumerGroupsSuccess(data)) onSuccess?.(data) @@ -516,28 +505,17 @@ export function fetchConsumers( try { const state = stateInit() - const { status } = await apiService.post( + const { data, status } = await apiService.post( getUrl( state.app.context.contextInstanceId, - ApiEndpoints.STREAMS_CONSUMER_GROUPS + ApiEndpoints.STREAMS_CONSUMERS_GET ), { keyName: state.app.context.browser.keyList.selectedKey, - count: 500, - sortOrder: SortOrder.ASC + groupName: state.browser.stream.groups.selectedGroup?.name, }, ) - const data = [] - - for (let i = 0; i < 1000; i++) { - data.push({ - name: `consumer ${i}`, - pending: Math.floor(Math.random() * 10), - time: Date.now() - }) - } - if (isStatusSuccessful(status)) { dispatch(loadConsumersSuccess(data)) onSuccess?.(data) @@ -568,7 +546,7 @@ export function fetchConsumerMessages( const { status } = await apiService.post( getUrl( state.app.context.contextInstanceId, - ApiEndpoints.STREAMS_CONSUMER_GROUPS + ApiEndpoints.STREAMS_CONSUMER_GROUPS_GET ), { keyName: state.app.context.browser.keyList.selectedKey, diff --git a/redisinsight/ui/src/slices/interfaces/stream.ts b/redisinsight/ui/src/slices/interfaces/stream.ts index e11f94afff..73276aa94b 100644 --- a/redisinsight/ui/src/slices/interfaces/stream.ts +++ b/redisinsight/ui/src/slices/interfaces/stream.ts @@ -1,4 +1,8 @@ -import { GetStreamEntriesResponse, StreamEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' +import { + ConsumerGroupDto, + GetStreamEntriesResponse, + StreamEntryDto, +} from 'apiSrc/modules/browser/dto/stream.dto' import { SortOrder } from 'uiSrc/constants' import { Nullable } from 'uiSrc/utils' @@ -27,7 +31,7 @@ export interface StateStream { export interface StateConsumerGroups { loading: boolean error: string - data: StreamEntryDto[] + data: ConsumerGroupDto[] selectedGroup: Nullable } From 8c605c35468976596f8b7273d543c8ef89a73466 Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Wed, 25 May 2022 16:31:19 +0400 Subject: [PATCH 05/20] removed context --- redisinsight/ui/src/slices/browser/stream.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/redisinsight/ui/src/slices/browser/stream.ts b/redisinsight/ui/src/slices/browser/stream.ts index 42f7634fe5..ea0bafe034 100644 --- a/redisinsight/ui/src/slices/browser/stream.ts +++ b/redisinsight/ui/src/slices/browser/stream.ts @@ -470,11 +470,11 @@ export function fetchConsumerGroups( const state = stateInit() const { data, status } = await apiService.post( getUrl( - state.app.context.contextInstanceId, + state.connections.instances.connectedInstance?.id, ApiEndpoints.STREAMS_CONSUMER_GROUPS_GET ), { - keyName: state.app.context.browser.keyList.selectedKey, + keyName: state.browser.keys.selectedKey.data?.name, }, ) @@ -507,11 +507,11 @@ export function fetchConsumers( const state = stateInit() const { data, status } = await apiService.post( getUrl( - state.app.context.contextInstanceId, + state.connections.instances.connectedInstance?.id, ApiEndpoints.STREAMS_CONSUMERS_GET ), { - keyName: state.app.context.browser.keyList.selectedKey, + keyName: state.browser.keys.selectedKey.data?.name, groupName: state.browser.stream.groups.selectedGroup?.name, }, ) @@ -545,11 +545,11 @@ export function fetchConsumerMessages( const state = stateInit() const { status } = await apiService.post( getUrl( - state.app.context.contextInstanceId, + state.connections.instances.connectedInstance?.id, ApiEndpoints.STREAMS_CONSUMER_GROUPS_GET ), { - keyName: state.app.context.browser.keyList.selectedKey, + keyName: state.browser.keys.selectedKey.data?.name, count: 500, sortOrder: SortOrder.ASC }, From fdfef1caf73095423839102d513b3a4c67dce50f Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Thu, 26 May 2022 10:24:42 +0400 Subject: [PATCH 06/20] Added pending messages --- .../ui/src/components/range-filter/index.ts | 3 + .../virtual-table/styles.module.scss | 9 +- redisinsight/ui/src/constants/api.ts | 1 + .../stream-details/StreamDetailsWrapper.tsx | 195 +++++++++++++++++- .../ConsumersView/ConsumersView.tsx | 13 +- .../ConsumersView/styles.module.scss | 2 +- .../consumers-view/ConsumersViewWrapper.tsx | 89 ++------ .../groups-view/GroupsView/GroupsView.tsx | 11 +- .../groups-view/GroupsView/styles.module.scss | 2 +- .../groups-view/GroupsViewWrapper.tsx | 71 ++++--- .../MessagesView/MessagesView.spec.tsx | 12 ++ .../MessagesView/MessagesView.tsx | 81 ++++++++ .../messages-view/MessagesView/index.ts | 3 + .../MessagesView/styles.module.scss | 82 ++++++++ .../MessagesViewWrapper.spec.tsx | 12 ++ .../messages-view/MessagesViewWrapper.tsx | 114 ++++++++++ .../stream-details/messages-view/index.ts | 3 + .../StreamDataViewWrapper.tsx | 1 + ...tails.spec.tsx => StreamDataView.spec.tsx} | 0 .../streamDataView/StreamDataView.tsx | 176 +--------------- .../streamDataView/styles.module.scss | 21 +- .../stream-details/stream-tabs/StreamTabs.tsx | 36 +++- .../stream-details/styles.module.scss | 28 +++ redisinsight/ui/src/slices/browser/keys.ts | 7 +- redisinsight/ui/src/slices/browser/stream.ts | 39 ++-- .../ui/src/slices/interfaces/stream.ts | 21 +- .../ui/src/styles/components/_components.scss | 27 +-- .../ui/src/styles/components/_tabs.scss | 51 +++++ redisinsight/ui/src/utils/streamUtils.ts | 13 ++ 29 files changed, 752 insertions(+), 371 deletions(-) create mode 100644 redisinsight/ui/src/components/range-filter/index.ts create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesView/MessagesView.spec.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesView/MessagesView.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesView/index.ts create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesView/styles.module.scss create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesViewWrapper.spec.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesViewWrapper.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/messages-view/index.ts rename redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/{StreamDetails.spec.tsx => StreamDataView.spec.tsx} (100%) create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/styles.module.scss create mode 100644 redisinsight/ui/src/styles/components/_tabs.scss diff --git a/redisinsight/ui/src/components/range-filter/index.ts b/redisinsight/ui/src/components/range-filter/index.ts new file mode 100644 index 0000000000..c754538242 --- /dev/null +++ b/redisinsight/ui/src/components/range-filter/index.ts @@ -0,0 +1,3 @@ +import RangeFilter from './RangeFilter' + +export default RangeFilter diff --git a/redisinsight/ui/src/components/virtual-table/styles.module.scss b/redisinsight/ui/src/components/virtual-table/styles.module.scss index 4ca2f73678..faac7424fc 100644 --- a/redisinsight/ui/src/components/virtual-table/styles.module.scss +++ b/redisinsight/ui/src/components/virtual-table/styles.module.scss @@ -151,12 +151,6 @@ $footerHeight: 38px; white-space: pre-wrap; } -:global(.key-details-table) { - :global(.ReactVirtualized__Table__row) { - font-size: 13px; - } -} - :global(.key-list-table) { height: calc(100% - 58px); } @@ -176,6 +170,9 @@ $footerHeight: 38px; } } } + :global(.ReactVirtualized__Table__row) { + font-size: 13px; + } :global(.ReactVirtualized__Table__headerRow) { border: 1px solid var(--tableLightestBorderColor) !important; } diff --git a/redisinsight/ui/src/constants/api.ts b/redisinsight/ui/src/constants/api.ts index a903cbc127..f70a75687e 100644 --- a/redisinsight/ui/src/constants/api.ts +++ b/redisinsight/ui/src/constants/api.ts @@ -38,6 +38,7 @@ enum ApiEndpoints { STREAMS_CONSUMER_GROUPS = 'streams/consumer-groups', STREAMS_CONSUMER_GROUPS_GET = 'streams/consumer-groups/get', STREAMS_CONSUMERS_GET = 'streams/consumer-groups/consumers/get', + STREAMS_CONSUMERS_MESSAGES_GET = 'streams/consumer-groups/consumers/pending-messages/get', STREAMS = 'streams', CLI = 'cli', CLI_BLOCKING_COMMANDS = 'info/cli-blocking-commands', diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetailsWrapper.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetailsWrapper.tsx index 42e059bcca..90a764fc75 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetailsWrapper.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetailsWrapper.tsx @@ -1,30 +1,184 @@ -import { EuiProgress, EuiTab, EuiTabs } from '@elastic/eui' -import React from 'react' +import { EuiProgress } from '@elastic/eui' +import React, { useCallback, useEffect, useMemo } from 'react' import { useDispatch, useSelector } from 'react-redux' +import { isNull, last } from 'lodash' +import cx from 'classnames' -import { streamSelector, setStreamViewType, streamGroupsSelector } from 'uiSrc/slices/browser/stream' -import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { + streamSelector, + streamGroupsSelector, + streamRangeSelector, + streamDataSelector, + fetchMoreStreamEntries, + updateStart, + updateEnd, + fetchStreamEntries +} from 'uiSrc/slices/browser/stream' import { StreamViewType } from 'uiSrc/slices/interfaces/stream' -import { streamViewTypeTabs } from './constants' +import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { getNextId, getTimestampFromId } from 'uiSrc/utils/streamUtils' +import { SortOrder } from 'uiSrc/constants' +import { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api' +import { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { GetStreamEntriesResponse } from 'apiSrc/modules/browser/dto/stream.dto' +import RangeFilter from 'uiSrc/components/range-filter' import ConsumersViewWrapper from './consumers-view' import GroupsViewWrapper from './groups-view' +import MessagesViewWrapper from './messages-view' import StreamDataViewWrapper from './stream-data-view' import StreamTabs from './stream-tabs' +import styles from './styles.module.scss' + export interface Props { isFooterOpen: boolean } const StreamDetailsWrapper = (props: Props) => { - const { id: instanceId } = useSelector(connectedInstanceSelector) - const { viewType, loading } = useSelector(streamSelector) + const { viewType, loading, sortOrder: entryColumnSortOrder } = useSelector(streamSelector) const { loading: loadingGroups } = useSelector(streamGroupsSelector) + const { start, end } = useSelector(streamRangeSelector) + const { + firstEntry, + lastEntry, + entries, + } = useSelector(streamDataSelector) + const { name: key } = useSelector(selectedKeyDataSelector) ?? { name: '' } + const { id: instanceId } = useSelector(connectedInstanceSelector) const dispatch = useDispatch() + const firstEntryTimeStamp = useMemo(() => getTimestampFromId(firstEntry?.id), [firstEntry?.id]) + const lastEntryTimeStamp = useMemo(() => getTimestampFromId(lastEntry?.id), [lastEntry?.id]) + + const startNumber = useMemo(() => (start === '' ? 0 : parseInt(start, 10)), [start]) + const endNumber = useMemo(() => (end === '' ? 0 : parseInt(end, 10)), [end]) + + const shouldFilterRender = !isNull(firstEntry) + && (firstEntry.id !== '') + && !isNull(lastEntry) + && lastEntry.id !== '' + + useEffect(() => { + if (isNull(firstEntry)) { + dispatch(updateStart('')) + } + if (start === '' && firstEntry?.id !== '') { + dispatch(updateStart(firstEntryTimeStamp.toString())) + } + }, [firstEntryTimeStamp]) + + useEffect(() => { + if (isNull(lastEntry)) { + dispatch(updateEnd('')) + } + if (end === '' && lastEntry?.id !== '') { + dispatch(updateEnd(lastEntryTimeStamp.toString())) + } + }, [lastEntryTimeStamp]) + + const loadMoreItems = () => { + const lastLoadedEntryId = last(entries)?.id + const lastLoadedEntryTimeStamp = getTimestampFromId(lastLoadedEntryId) + + const lastRangeEntryTimestamp = end ? parseInt(end, 10) : getTimestampFromId(lastEntry?.id) + const firstRangeEntryTimestamp = start ? parseInt(start, 10) : getTimestampFromId(firstEntry?.id) + const shouldLoadMore = () => { + if (!lastLoadedEntryTimeStamp) { + return false + } + return entryColumnSortOrder === SortOrder.ASC + ? lastLoadedEntryTimeStamp <= lastRangeEntryTimestamp + : lastLoadedEntryTimeStamp >= firstRangeEntryTimestamp + } + const nextId = getNextId(lastLoadedEntryId, entryColumnSortOrder) + + if (shouldLoadMore()) { + dispatch( + fetchMoreStreamEntries( + key, + entryColumnSortOrder === SortOrder.DESC ? start : nextId, + entryColumnSortOrder === SortOrder.DESC ? nextId : end, + SCAN_COUNT_DEFAULT, + entryColumnSortOrder, + ) + ) + } + } + + const filterTelemetry = (data: GetStreamEntriesResponse) => { + sendEventTelemetry({ + event: TelemetryEvent.STREAM_DATA_FILTERED, + eventData: { + databaseId: instanceId, + total: data.total, + } + }) + } + + const resetFilterTelemetry = (data: GetStreamEntriesResponse) => { + sendEventTelemetry({ + event: TelemetryEvent.STREAM_DATA_FILTER_RESET, + eventData: { + databaseId: instanceId, + total: data.total, + } + }) + } + + const loadEntries = (telemetryAction?: (data: GetStreamEntriesResponse) => void) => { + dispatch(fetchStreamEntries( + key, + SCAN_COUNT_DEFAULT, + entryColumnSortOrder, + false, + telemetryAction + )) + } + + const handleChangeStartFilter = useCallback( + (value: number, shouldSentEventTelemetry: boolean) => { + dispatch(updateStart(value.toString())) + loadEntries(shouldSentEventTelemetry ? filterTelemetry : undefined) + }, + [] + ) + + const handleChangeEndFilter = useCallback( + (value: number, shouldSentEventTelemetry: boolean) => { + dispatch(updateEnd(value.toString())) + loadEntries(shouldSentEventTelemetry ? filterTelemetry : undefined) + }, + [] + ) + + const handleResetFilter = useCallback( + () => { + dispatch(updateStart(firstEntryTimeStamp.toString())) + dispatch(updateEnd(lastEntryTimeStamp.toString())) + loadEntries(resetFilterTelemetry) + }, + [lastEntryTimeStamp, firstEntryTimeStamp] + ) + + const handleUpdateRangeMin = useCallback( + (min: number) => { + dispatch(updateStart(min.toString())) + }, + [] + ) + + const handleUpdateRangeMax = useCallback( + (max: number) => { + dispatch(updateEnd(max.toString())) + }, + [] + ) + return ( - <> +
{(loading || loadingGroups) && ( { data-testid="progress-key-stream" /> )} + {shouldFilterRender ? ( + + ) + : ( +
+
+
+ )} {viewType === StreamViewType.Data && ( - + )} {viewType === StreamViewType.Groups && ( @@ -43,7 +215,10 @@ const StreamDetailsWrapper = (props: Props) => { {viewType === StreamViewType.Consumers && ( )} - + {viewType === StreamViewType.Messages && ( + + )} +
) } diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersView/ConsumersView.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersView/ConsumersView.tsx index 105e5df1a1..f81fba3286 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersView/ConsumersView.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersView/ConsumersView.tsx @@ -10,7 +10,7 @@ import VirtualTable from 'uiSrc/components/virtual-table/VirtualTable' import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces' import { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys' import { SortOrder } from 'uiSrc/constants' -import { StreamEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' +import { ConsumerDto } from 'apiSrc/modules/browser/dto/stream.dto' import styles from './styles.module.scss' @@ -18,16 +18,11 @@ const headerHeight = 60 const rowHeight = 54 const actionsWidth = 54 const minColumnWidth = 190 -const noItemsMessageString = 'There are no Consumers in the Group.' - -interface IStreamEntry extends StreamEntryDto { - editing: boolean -} +const noItemsMessageString = 'Your Consumer Group has no Consumers available.' export interface Props { - data: IStreamEntry[] + data: ConsumerDto[] columns: ITableColumn[] - onEditConsumer: (consumerId:string, editing: boolean) => void onClosePopover: () => void onSelectConsumer: ({ rowData }: { rowData: any }) => void isFooterOpen?: boolean @@ -55,7 +50,7 @@ const ConsumersView = (props: Props) => {
{ const dispatch = useDispatch() - const [consumers, setConsumers] = useState([]) + const [consumers, setConsumers] = useState(loadedConsumers) const [deleting, setDeleting] = useState('') - useEffect(() => { - dispatch(fetchConsumers()) - }, []) - - useEffect(() => { - const streamConsumers: IStreamEntry[] = loadedConsumers?.map((item) => ({ - ...item, - editing: false, - })) - - setConsumers(streamConsumers) - }, [loadedConsumers, deleting]) - const closePopover = useCallback(() => { setDeleting('') }, []) - const showPopover = useCallback((entry = '') => { - setDeleting(`${entry + suffix}`) + const showPopover = useCallback((consumer = '') => { + setDeleting(`${consumer + suffix}`) }, []) - const handleDeleteConsumer = (entryId = '') => { - dispatch(deleteStreamEntry(key, [entryId])) + const handleDeleteConsumer = (consumerName = '') => { + dispatch(deleteStreamEntry(key, [consumerName])) closePopover() } @@ -84,21 +66,11 @@ const ConsumersViewWrapper = (props: Props) => { // }) } - const handleEditConsumer = (consumerId = '', editing: boolean) => { - const newConsumersState = consumers.map((item) => { - if (item.id === consumerId) { - return { ...item, editing } - } - return item - }) - setConsumers(newConsumersState) - } - const handleSelectConsumer = ({ rowData }: { rowData: any }) => { dispatch(setSelectedConsumer(rowData)) - dispatch(fetchConsumers( - true, - () => dispatch(setStreamViewType(StreamViewType.Groups)) + dispatch(fetchConsumerMessages( + false, + () => dispatch(setStreamViewType(StreamViewType.Messages)) )) } @@ -110,8 +82,6 @@ const ConsumersViewWrapper = (props: Props) => { // minWidth: 180, truncateText: true, isSortable: true, - // render: (cellData: ConnectionType) => - // capitalize(cellData), }, { id: 'pending', @@ -120,34 +90,16 @@ const ConsumersViewWrapper = (props: Props) => { absoluteWidth: 106, truncateText: true, isSortable: true, - // render: (cellData: ConnectionType) => - // capitalize(cellData), }, { - id: 'time', + id: 'idle', label: 'Idle time, ms', absoluteWidth: 190, minWidth: 190, isSortable: true, className: styles.cell, headerClassName: styles.cellHeader, - render: function Id(_name: string, { lastDeliveredId: id }: StreamEntryDto) { - const timestamp = id?.split('-')?.[0] - return ( -
- -
- {getFormatTime(timestamp)} -
-
- -
- {id} -
-
-
- ) - }, + render: (cellData: number) => numberWithSpaces(cellData), }, { id: 'actions', @@ -157,24 +109,24 @@ const ConsumersViewWrapper = (props: Props) => { absoluteWidth: actionsWidth, maxWidth: actionsWidth, minWidth: actionsWidth, - render: function Actions(_act: any, { id }: StreamEntryDto) { + render: function Actions(_act: any, { name }: ConsumerDto) { return (
- Consumers will be removed from + Consumer will be removed from
{key} )} - item={id} + item={name} suffix={suffix} deleting={deleting} closePopover={closePopover} updateLoading={false} showPopover={showPopover} - testid={`remove-consumers-button-${id}`} + testid={`remove-consumer-button-${name}`} handleDeleteItem={handleDeleteConsumer} handleButtonClick={handleRemoveIconClick} /> @@ -189,7 +141,6 @@ const ConsumersViewWrapper = (props: Props) => { void onClosePopover: () => void @@ -56,7 +55,7 @@ const ConsumerGroups = (props: Props) => {
{ const dispatch = useDispatch() - const [groups, setGroups] = useState([]) + const [groups, setGroups] = useState([]) const [deleting, setDeleting] = useState('') useEffect(() => { - dispatch(fetchConsumerGroups()) - }, []) - - useEffect(() => { - const streamGroups: IStreamEntry[] = loadedGroups?.map((item) => ({ + const streamGroups: IConsumerGroup[] = loadedGroups?.map((item) => ({ ...item, editing: false, })) @@ -55,12 +57,12 @@ const GroupsViewWrapper = (props: Props) => { setDeleting('') }, []) - const showPopover = useCallback((entry = '') => { - setDeleting(`${entry + suffix}`) + const showPopover = useCallback((groupName = '') => { + setDeleting(`${groupName + suffix}`) }, []) - const handleDeleteGroup = (entryId = '') => { - dispatch(deleteStreamEntry(key, [entryId])) + const handleDeleteGroup = (groupName = '') => { + // dispatch(deleteStreamEntry(key, [groupName])) closePopover() } @@ -80,7 +82,7 @@ const GroupsViewWrapper = (props: Props) => { const handleEditGroup = (groupId = '', editing: boolean) => { const newGroupsState = groups.map((item) => { - if (item.id === groupId) { + if (item.name === groupId) { return { ...item, editing } } return item @@ -91,7 +93,7 @@ const GroupsViewWrapper = (props: Props) => { const handleSelectGroup = ({ rowData }: { rowData: any }) => { dispatch(setSelectedGroup(rowData)) dispatch(fetchConsumers( - true, + false, () => dispatch(setStreamViewType(StreamViewType.Consumers)) )) } @@ -122,10 +124,33 @@ const GroupsViewWrapper = (props: Props) => { label: 'Pending', minWidth: 106, absoluteWidth: 106, - truncateText: true, isSortable: true, - // render: (cellData: ConnectionType) => - // capitalize(cellData), + className: styles.cell, + headerClassName: styles.cellHeader, + render: function P(_name: string, { pending, greatestPendingId, smallestPendingId, name }: ConsumerGroupDto) { + const smallestTimestamp = smallestPendingId?.split('-')?.[0] + const greatestTimestamp = greatestPendingId?.split('-')?.[0] + + const tooltipContent = `${getFormatTime(smallestTimestamp)}-${getFormatTime(greatestTimestamp)}` + return ( + +
+ {!!pending && ( + + <>{pending} + + )} + {!pending && pending} +
+
+ ) + }, }, { id: 'lastDeliveredId', @@ -135,7 +160,7 @@ const GroupsViewWrapper = (props: Props) => { isSortable: true, className: styles.cell, headerClassName: styles.cellHeader, - render: function Id(_name: string, { lastDeliveredId: id }: StreamEntryDto) { + render: function Id(_name: string, { lastDeliveredId: id }: ConsumerGroupDto) { const timestamp = id?.split('-')?.[0] return (
@@ -161,13 +186,13 @@ const GroupsViewWrapper = (props: Props) => { absoluteWidth: actionsWidth, maxWidth: actionsWidth, minWidth: actionsWidth, - render: function Actions(_act: any, { id }: StreamEntryDto) { + render: function Actions(_act: any, { id }: ConsumerGroupDto) { return (
- Groups will be removed from + Group will be removed from
{key} diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesView/MessagesView.spec.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesView/MessagesView.spec.tsx new file mode 100644 index 0000000000..61349714c6 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesView/MessagesView.spec.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import GroupsView, { Props } from './MessagesView' + +const mockedProps = mock() + +describe('GroupsView', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesView/MessagesView.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesView/MessagesView.tsx new file mode 100644 index 0000000000..eb267cea40 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesView/MessagesView.tsx @@ -0,0 +1,81 @@ +import React, { useState } from 'react' +import { useSelector } from 'react-redux' +import cx from 'classnames' +import { orderBy } from 'lodash' + +import { + streamGroupsSelector, +} from 'uiSrc/slices/browser/stream' +import VirtualTable from 'uiSrc/components/virtual-table/VirtualTable' +import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces' +import { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys' +import { SortOrder } from 'uiSrc/constants' +import { PendingEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' + +import styles from './styles.module.scss' + +const headerHeight = 60 +const rowHeight = 54 + +const noItemsMessageString = 'There are no Messages in the Consumer Group.' + +export interface Props { + data: PendingEntryDto[] + columns: ITableColumn[] + onClosePopover: () => void + isFooterOpen?: boolean +} + +const MessagesView = (props: Props) => { + const { data = [], columns = [], onClosePopover, isFooterOpen } = props + + const { loading } = useSelector(streamGroupsSelector) + const { name: key = '' } = useSelector(selectedKeyDataSelector) ?? { } + + const [messages, setMessages] = useState(data) + const [sortedColumnName, setSortedColumnName] = useState('id') + const [sortedColumnOrder, setSortedColumnOrder] = useState(SortOrder.DESC) + + const onChangeSorting = (column: any, order: SortOrder) => { + setSortedColumnName(column) + setSortedColumnOrder(order) + + setMessages(orderBy(messages, 'name', order?.toLowerCase())) + } + + return ( + <> +
+ +
+ + ) +} + +export default MessagesView diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesView/index.ts b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesView/index.ts new file mode 100644 index 0000000000..627dd57905 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesView/index.ts @@ -0,0 +1,3 @@ +import GroupsView from './MessagesView' + +export default GroupsView diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesView/styles.module.scss b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesView/styles.module.scss new file mode 100644 index 0000000000..0e64475c55 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesView/styles.module.scss @@ -0,0 +1,82 @@ +.container { + display: flex; + flex: 1; + width: 100%; + padding-top: 3px; + background-color: var(--euiColorEmptyShade); + + :global { + .ReactVirtualized__Grid__innerScrollContainer { + .ReactVirtualized__Table__rowColumn { + border-right: 1px solid var(--tableDarkestBorderColor) !important; + + &:last-of-type, + &:nth-last-of-type(2) { + border-right: none !important; + } + } + + .ReactVirtualized__Table__row { + border-bottom: 1px solid var(--tableDarkestBorderColor) !important; + &:last-of-type { + border-bottom: none !important; + } + } + + & > div:hover { + background: var(--euiColorLightestShade); + + .value-table-actions { + background-color: var(--euiColorLightestShade) !important; + } + + .streamEntry { + color: var(--inputTextColor) !important; + } + + .streamEntryId { + color: var(--euiTextSubduedColor) !important; + } + } + } + + .ReactVirtualized__Table__headerRow { + border: none !important; + } + + .ReactVirtualized__Table__Grid { + border: 1px solid var(--tableDarkestBorderColor) !important; + } + } + + .cellHeader { + border: none !important; + } +} + +:global(.streamEntry) { + color: var(--euiTextSubduedColor) !important; + white-space: normal; + max-width: 100%; + word-break: break-all; +} + +:global(.streamEntryId) { + color: var(--euiColorMediumShade) !important; + display: flex; +} + +:global(.stream-entry-actions) { + margin-left: -5px; +} + +.actions, +.actionsHeader { + width: 54px; +} + +.actions { + :global(.value-table-actions) { + background-color: var(--euiColorEmptyShade) !important; + } +} diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesViewWrapper.spec.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesViewWrapper.spec.tsx new file mode 100644 index 0000000000..d1f098b65f --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesViewWrapper.spec.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import MessagesViewWrapper, { Props } from './MessagesViewWrapper' + +const mockedProps = mock() + +describe('MessagesViewWrapper', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesViewWrapper.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesViewWrapper.tsx new file mode 100644 index 0000000000..bd482a95e8 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesViewWrapper.tsx @@ -0,0 +1,114 @@ +import { EuiText } from '@elastic/eui' +import React, { useCallback, useState } from 'react' +import { useSelector } from 'react-redux' + +import { selectedConsumerSelector } from 'uiSrc/slices/browser/stream' +import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces' +import { getFormatTime } from 'uiSrc/utils/streamUtils' +import { TableCellTextAlignment } from 'uiSrc/constants' +import { PendingEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' + +import MessagesView from './MessagesView' + +import styles from './MessagesView/styles.module.scss' + +const actionsWidth = 50 +const minColumnWidth = 190 +const suffix = '_stream_messages' + +interface Props { + isFooterOpen: boolean +} + +const MessagesViewWrapper = (props: Props) => { + const { + data: loadedMessages = [], + } = useSelector(selectedConsumerSelector) ?? {} + + const [messages, setMessages] = useState(loadedMessages) + const [claiming, setClaiming] = useState('') + + const closePopover = useCallback(() => { + setClaiming('') + }, []) + + const showPopover = useCallback((consumer = '') => { + setClaiming(`${consumer + suffix}`) + }, []) + + const columns: ITableColumn[] = [ + { + id: 'id', + label: 'Entry ID', + absoluteWidth: minColumnWidth, + minWidth: minColumnWidth, + isSortable: true, + className: styles.cell, + headerClassName: styles.cellHeader, + render: function Id(_name: string, { id }: PendingEntryDto) { + const timestamp = id.split('-')?.[0] + return ( +
+ +
+ {getFormatTime(timestamp)} +
+
+ +
+ {id} +
+
+
+ ) + }, + }, + { + id: 'idle', + label: 'Last Message Delivered', + minWidth: 256, + absoluteWidth: 106, + truncateText: true, + isSortable: true, + // render: (cellData: ConnectionType) => + // capitalize(cellData), + }, + { + id: 'delivered', + label: 'Times Message Delivered', + minWidth: 106, + absoluteWidth: 106, + truncateText: true, + isSortable: true, + // render: (cellData: ConnectionType) => + // capitalize(cellData), + }, + { + id: 'actions', + label: '', + headerClassName: styles.actionsHeader, + textAlignment: TableCellTextAlignment.Left, + absoluteWidth: actionsWidth, + maxWidth: actionsWidth, + minWidth: actionsWidth, + render: function Actions(_act: any, { id }: PendingEntryDto) { + return ( +
+ ) + }, + }, + ] + + return ( + <> + + + ) +} + +export default MessagesViewWrapper diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/index.ts b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/index.ts new file mode 100644 index 0000000000..183a0b53b9 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/index.ts @@ -0,0 +1,3 @@ +import MessagesViewWrapper from './MessagesViewWrapper' + +export default MessagesViewWrapper diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx index 32ac5dffaa..0787fab750 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx @@ -27,6 +27,7 @@ const minColumnWidth = 190 export interface Props { isFooterOpen: boolean + loadMoreItems: () => void } const StreamDataViewWrapper = (props: Props) => { diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/StreamDetails.spec.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/StreamDataView.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/StreamDetails.spec.tsx rename to redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/StreamDataView.spec.tsx diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/StreamDataView.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/StreamDataView.tsx index 8d0b443cff..863c63eb1d 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/StreamDataView.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/StreamDataView.tsx @@ -1,27 +1,19 @@ -import React, { useCallback, useMemo, useState, useEffect } from 'react' +import React, { useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { last, isNull } from 'lodash' +import { isNull } from 'lodash' import cx from 'classnames' import { - fetchMoreStreamEntries, fetchStreamEntries, - updateStart, - updateEnd, streamDataSelector, streamSelector, - streamRangeSelector, } from 'uiSrc/slices/browser/stream' import VirtualTable from 'uiSrc/components/virtual-table/VirtualTable' import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces' -import RangeFilter from 'uiSrc/components/range-filter/RangeFilter' import { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys' import { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api' import { SortOrder } from 'uiSrc/constants' -import { getTimestampFromId } from 'uiSrc/utils/streamUtils' -import { StreamEntryDto, GetStreamEntriesResponse } from 'apiSrc/modules/browser/dto/stream.dto' -import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { StreamEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' import styles from './styles.module.scss' @@ -36,104 +28,30 @@ interface IStreamEntry extends StreamEntryDto { editing: boolean } -const getNextId = (id: string, sortOrder: SortOrder): string => { - const splittedId = id.split('-') - // if we don't have prefix - if (splittedId.length === 1) { - return `${id}-1` - } - if (sortOrder === SortOrder.DESC) { - return splittedId[1] === '0' ? `${parseInt(splittedId[0], 10) - 1}` : `${splittedId[0]}-${+splittedId[1] - 1}` - } - return `${splittedId[0]}-${+splittedId[1] + 1}` -} - export interface Props { data: IStreamEntry[] columns: ITableColumn[] onEditEntry: (entryId:string, editing: boolean) => void onClosePopover: () => void + loadMoreItems: () => void isFooterOpen?: boolean } const StreamDataView = (props: Props) => { - const { data: entries = [], columns = [], onClosePopover, isFooterOpen } = props + const { data: entries = [], columns = [], onClosePopover, loadMoreItems, isFooterOpen } = props const dispatch = useDispatch() const { loading } = useSelector(streamSelector) - const { start, end } = useSelector(streamRangeSelector) const { total, firstEntry, lastEntry, } = useSelector(streamDataSelector) const { name: key } = useSelector(selectedKeyDataSelector) ?? { name: '' } - const { id: instanceId } = useSelector(connectedInstanceSelector) - - const shouldFilterRender = !isNull(firstEntry) && (firstEntry.id !== '') && !isNull(lastEntry) && lastEntry.id !== '' const [sortedColumnName, setSortedColumnName] = useState('id') const [sortedColumnOrder, setSortedColumnOrder] = useState(SortOrder.DESC) - const loadMoreItems = () => { - const lastLoadedEntryId = last(entries)?.id - const lastLoadedEntryTimeStamp = getTimestampFromId(lastLoadedEntryId) - - const lastRangeEntryTimestamp = end ? parseInt(end, 10) : getTimestampFromId(lastEntry?.id) - const firstRangeEntryTimestamp = start ? parseInt(start, 10) : getTimestampFromId(firstEntry?.id) - const shouldLoadMore = () => { - if (!lastLoadedEntryTimeStamp) { - return false - } - return sortedColumnOrder === SortOrder.ASC - ? lastLoadedEntryTimeStamp <= lastRangeEntryTimestamp - : lastLoadedEntryTimeStamp >= firstRangeEntryTimestamp - } - const nextId = getNextId(lastLoadedEntryId, sortedColumnOrder) - - if (shouldLoadMore()) { - dispatch( - fetchMoreStreamEntries( - key, - sortedColumnOrder === SortOrder.DESC ? start : nextId, - sortedColumnOrder === SortOrder.DESC ? nextId : end, - SCAN_COUNT_DEFAULT, - sortedColumnOrder, - ) - ) - } - } - - const filterTelementry = (data: GetStreamEntriesResponse) => { - sendEventTelemetry({ - event: TelemetryEvent.STREAM_DATA_FILTERED, - eventData: { - databaseId: instanceId, - total: data.total, - } - }) - } - - const resetFilterTelementry = (data: GetStreamEntriesResponse) => { - sendEventTelemetry({ - event: TelemetryEvent.STREAM_DATA_FILTER_RESET, - eventData: { - databaseId: instanceId, - total: data.total, - } - }) - } - - const loadEntries = (telemetryAction?: (data: GetStreamEntriesResponse) => void) => { - dispatch(fetchStreamEntries( - key, - SCAN_COUNT_DEFAULT, - sortedColumnOrder, - false, - telemetryAction - )) - } - const onChangeSorting = (column: any, order: SortOrder) => { setSortedColumnName(column) setSortedColumnOrder(order) @@ -141,93 +59,13 @@ const StreamDataView = (props: Props) => { dispatch(fetchStreamEntries(key, SCAN_COUNT_DEFAULT, order, false)) } - const handleChangeStartFilter = useCallback( - (value: number, shouldSentEventTelemetry: boolean) => { - dispatch(updateStart(value.toString())) - loadEntries(shouldSentEventTelemetry ? filterTelementry : undefined) - }, - [] - ) - - const handleChangeEndFilter = useCallback( - (value: number, shouldSentEventTelemetry: boolean) => { - dispatch(updateEnd(value.toString())) - loadEntries(shouldSentEventTelemetry ? filterTelementry : undefined) - }, - [] - ) - - const firstEntryTimeStamp = useMemo(() => getTimestampFromId(firstEntry?.id), [firstEntry?.id]) - const lastEntryTimeStamp = useMemo(() => getTimestampFromId(lastEntry?.id), [lastEntry?.id]) - - const startNumber = useMemo(() => (start === '' ? 0 : parseInt(start, 10)), [start]) - const endNumber = useMemo(() => (end === '' ? 0 : parseInt(end, 10)), [end]) - - const handleResetFilter = useCallback( - () => { - dispatch(updateStart(firstEntryTimeStamp.toString())) - dispatch(updateEnd(lastEntryTimeStamp.toString())) - loadEntries(resetFilterTelementry) - }, - [lastEntryTimeStamp, firstEntryTimeStamp] - ) - - const handleUpdateRangeMin = useCallback( - (min: number) => { - dispatch(updateStart(min.toString())) - }, - [] - ) - - const handleUpdateRangeMax = useCallback( - (max: number) => { - dispatch(updateEnd(max.toString())) - }, - [] - ) - - useEffect(() => { - if (isNull(firstEntry)) { - dispatch(updateStart('')) - } - if (start === '' && firstEntry?.id !== '') { - dispatch(updateStart(firstEntryTimeStamp.toString())) - } - }, [firstEntryTimeStamp]) - - useEffect(() => { - if (isNull(lastEntry)) { - dispatch(updateEnd('')) - } - if (end === '' && lastEntry?.id !== '') { - dispatch(updateEnd(lastEntryTimeStamp.toString())) - } - }, [lastEntryTimeStamp]) - return ( <> - {shouldFilterRender ? ( - - ) - : ( -
-
-
- )} +
{ - const { viewType } = useSelector(streamSelector) + const { viewType, data: { entries = [] } } = useSelector(streamSelector) + const { data: groups = [] } = useSelector(streamGroupsSelector) const dispatch = useDispatch() const onSelectedTabChanged = (id: StreamViewType) => { dispatch(setStreamViewType(id)) + + if (id === StreamViewType.Data && entries?.length === 0) { + // dispatch(fetchConsumerGroups()) + } + if (id === StreamViewType.Groups && groups.length === 0) { + dispatch(fetchConsumerGroups()) + } + } + + const getSelectedTab = (id:StreamViewType) => { + if (id === StreamViewType.Data && viewType === id) { + return true + } + + if (id === StreamViewType.Groups + && (viewType === id || viewType === StreamViewType.Consumers || viewType === StreamViewType.Messages)) { + return true + } + + return false } const renderTabs = () => streamViewTypeTabs.map(({ id, label }, i) => ( onSelectedTabChanged(id)} // eslint-disable-next-line react/no-array-index-key key={i} @@ -29,7 +55,7 @@ const StreamTabs = () => { )) return ( - {renderTabs()} + {renderTabs()} ) } diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/styles.module.scss b/redisinsight/ui/src/pages/browser/components/stream-details/styles.module.scss new file mode 100644 index 0000000000..dc283b80b8 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/styles.module.scss @@ -0,0 +1,28 @@ +.container { + padding: 0 18px; + height: 100%; + position: relative; + + :global(.stream-details-table) { + height: calc(100% - 125px); + } +} + +.rangeWrapper { + margin: 30px 30px 26px; + padding: 12px 0; +} + +.sliderTrack.mockRange { + left: 18px; + width: calc(100% - 36px); +} + +.sliderTrack { + position: absolute; + background-color: var(--separatorColor); + width: 100%; + height: 1px; + margin-top: 2px; + z-index: 1; +} diff --git a/redisinsight/ui/src/slices/browser/keys.ts b/redisinsight/ui/src/slices/browser/keys.ts index 924972804a..87d7395070 100644 --- a/redisinsight/ui/src/slices/browser/keys.ts +++ b/redisinsight/ui/src/slices/browser/keys.ts @@ -556,12 +556,7 @@ export function fetchKeyInfo(key: string, resetData?: boolean) { resetData )) } else if (viewType === StreamViewType.Groups) { - dispatch(fetchConsumerGroups( - // key, - // SCAN_COUNT_DEFAULT, - // SortOrder.DESC, - // resetData - )) + // dispatch(fetchConsumerGroups()) } } } catch (_err) { diff --git a/redisinsight/ui/src/slices/browser/stream.ts b/redisinsight/ui/src/slices/browser/stream.ts index ea0bafe034..098bf95a8d 100644 --- a/redisinsight/ui/src/slices/browser/stream.ts +++ b/redisinsight/ui/src/slices/browser/stream.ts @@ -14,7 +14,7 @@ import { ConsumerDto, ConsumerGroupDto, GetStreamEntriesResponse, - StreamEntryDto, + PendingEntryDto, } from 'apiSrc/modules/browser/dto/stream.dto' import { AppDispatch, RootState } from '../store' import { StateStream, StreamViewType } from '../interfaces/stream' @@ -46,6 +46,7 @@ export const initialState: StateStream = { error: '', data: [], selectedGroup: null, + lastRefreshTime: null, } } @@ -146,6 +147,7 @@ const streamSlice = createSlice({ loadConsumerGroupsSuccess: (state, { payload }: PayloadAction) => { state.groups.loading = false state.groups.data = payload + state.groups.data.lastRefreshTime = Date.now() }, loadConsumerGroupsFailure: (state, { payload }) => { state.groups.loading = false @@ -167,18 +169,24 @@ const streamSlice = createSlice({ state.groups.selectedGroup = { ...state.groups.selectedGroup, - data: payload + data: { + ...payload, + lastRefreshTime: Date.now() + } } }, - loadConsumerMessagesSuccess: (state, { payload }: PayloadAction) => { + loadConsumerMessagesSuccess: (state, { payload }: PayloadAction) => { state.groups.loading = false state.groups.selectedGroup = { ...state.groups.selectedGroup, selectedConsumer: { ...state.groups.selectedGroup.selectedConsumer, - data: payload + data: { + ...payload, + lastRefreshTime: Date.now() + } } } }, @@ -228,7 +236,7 @@ export const streamGroupsSelector = (state: RootState) => state.browser.stream?. export const streamGroupsDataSelector = (state: RootState) => state.browser.stream?.groups?.data || [] export const selectedGroupSelector = (state: RootState) => state.browser.stream?.groups?.selectedGroup export const selectedConsumerSelector = (state: RootState) => - state.browser.stream?.groups?.selectedGroup?.selectedConsumer || {} + state.browser.stream?.groups?.selectedGroup?.selectedConsumer // The reducer export default streamSlice.reducer @@ -535,7 +543,7 @@ export function fetchConsumers( // Asynchronous thunk action export function fetchConsumerMessages( resetData?: boolean, - onSuccess?: (data: GetStreamEntriesResponse) => void, + onSuccess?: (data: PendingEntryDto[]) => void, onFailed?: () => void, ) { return async (dispatch: AppDispatch, stateInit: () => RootState) => { @@ -543,28 +551,17 @@ export function fetchConsumerMessages( try { const state = stateInit() - const { status } = await apiService.post( + const { data, status } = await apiService.post( getUrl( state.connections.instances.connectedInstance?.id, - ApiEndpoints.STREAMS_CONSUMER_GROUPS_GET + ApiEndpoints.STREAMS_CONSUMERS_MESSAGES_GET ), { keyName: state.browser.keys.selectedKey.data?.name, - count: 500, - sortOrder: SortOrder.ASC + groupName: state.browser.stream.groups.selectedGroup?.name, + consumerName: state.browser.stream.groups.selectedGroup?.selectedConsumer?.name }, ) - - const data = [] - - for (let i = 0; i < 1000; i++) { - data.push({ - entryId: `${Date.now()}-0`, - lastMessage: Date.now(), - timesDelivered: Math.floor(Math.random() * 10), - }) - } - if (isStatusSuccessful(status)) { dispatch(loadConsumerMessagesSuccess(data)) onSuccess?.(data) diff --git a/redisinsight/ui/src/slices/interfaces/stream.ts b/redisinsight/ui/src/slices/interfaces/stream.ts index 73276aa94b..c254ae73e2 100644 --- a/redisinsight/ui/src/slices/interfaces/stream.ts +++ b/redisinsight/ui/src/slices/interfaces/stream.ts @@ -1,10 +1,11 @@ +import { SortOrder } from 'uiSrc/constants' +import { Nullable } from 'uiSrc/utils' import { + ConsumerDto, ConsumerGroupDto, GetStreamEntriesResponse, - StreamEntryDto, + PendingEntryDto, } from 'apiSrc/modules/browser/dto/stream.dto' -import { SortOrder } from 'uiSrc/constants' -import { Nullable } from 'uiSrc/utils' type Range = { start: string @@ -33,24 +34,20 @@ export interface StateConsumerGroups { error: string data: ConsumerGroupDto[] selectedGroup: Nullable + lastRefreshTime: Nullable } export interface StateSelectedGroup { loading: boolean name: string - data: StreamEntryDto[] + data: ConsumerDto[] selectedConsumer: Nullable + lastRefreshTime: Nullable } export interface StateSelectedConsumer { loading: boolean name: string - data: StreamEntryDto[] - selectedMsg: Nullable -} - -export interface StateSelectedMessage { - loading: boolean - name: string - data: StreamEntryDto[] + data: PendingEntryDto[] + lastRefreshTime: Nullable } diff --git a/redisinsight/ui/src/styles/components/_components.scss b/redisinsight/ui/src/styles/components/_components.scss index 95cb4268e2..eed48382dd 100644 --- a/redisinsight/ui/src/styles/components/_components.scss +++ b/redisinsight/ui/src/styles/components/_components.scss @@ -1,15 +1,16 @@ // Import here all partials from current folder -@import 'tool_tip'; -@import 'forms'; -@import 'buttons'; -@import 'textarea'; -@import 'toasts'; -@import 'accordion'; -@import 'popover'; -@import 'table'; -@import 'badge'; -@import 'radio'; -@import 'resizable_container'; -@import 'database'; -@import 'switch'; +@import "tool_tip"; +@import "forms"; +@import "buttons"; +@import "textarea"; +@import "toasts"; +@import "accordion"; +@import "popover"; +@import "table"; +@import "badge"; +@import "radio"; +@import "resizable_container"; +@import "database"; +@import "switch"; @import "callout"; +@import "tabs"; diff --git a/redisinsight/ui/src/styles/components/_tabs.scss b/redisinsight/ui/src/styles/components/_tabs.scss new file mode 100644 index 0000000000..b0e8c300c1 --- /dev/null +++ b/redisinsight/ui/src/styles/components/_tabs.scss @@ -0,0 +1,51 @@ +@import "@elastic/eui/src/global_styling/mixins/helpers"; +@import "@elastic/eui/src/components/table/mixins"; +@import "@elastic/eui/src/global_styling/index"; + +.euiTabs { + &::before { + display: none !important; + } +} + +.euiTab__content { + font-size: 13px; + line-height: 18px; + font-weight: normal; +} + +.euiTab { + padding: 6px !important; + border-radius: 4px; + + &::before { + display: none !important; + } + + &:focus { + text-decoration: none !important; + } +} + +.euiTab.euiTab-isSelected { + background-color: var(--tableRowSelectedColor) !important; + &::after { + display: none !important; + } +} + +.euiTabs .euiTab + .euiTab { + margin-left: 18px; + + &::after { + display: block !important; + position: absolute; + animation: euiTab 150ms cubic-bezier(0.694, 0.0482, 0.335, 1); + background-color: var(--tableLightestBorderColor); + top: 7px; + content: " "; + height: 18px; + left: -10px; + width: 2px; + } +} diff --git a/redisinsight/ui/src/utils/streamUtils.ts b/redisinsight/ui/src/utils/streamUtils.ts index 845b7aad63..80219b56d8 100644 --- a/redisinsight/ui/src/utils/streamUtils.ts +++ b/redisinsight/ui/src/utils/streamUtils.ts @@ -1,4 +1,5 @@ import { format } from 'date-fns' +import { SortOrder } from 'uiSrc/constants' import { SCAN_STREAM_START_DEFAULT, SCAN_STREAM_END_DEFAULT } from 'uiSrc/constants/api' export const getFormatTime = (time: string = '') => @@ -20,3 +21,15 @@ export const getStreamRangeEnd = (end: string, endEtryId: string) => { } return end } + +export const getNextId = (id: string, sortOrder: SortOrder): string => { + const splittedId = id.split('-') + // if we don't have prefix + if (splittedId.length === 1) { + return `${id}-1` + } + if (sortOrder === SortOrder.DESC) { + return splittedId[1] === '0' ? `${parseInt(splittedId[0], 10) - 1}` : `${splittedId[0]}-${+splittedId[1] - 1}` + } + return `${splittedId[0]}-${+splittedId[1] + 1}` +} From 2c56f9fe799d262ed7fa970f0c0f509c85458cdc Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Thu, 26 May 2022 11:01:39 +0400 Subject: [PATCH 07/20] rename folder --- .../StreamDataView12/StreamDataView.spec.tsx | 12 ++ .../StreamDataView12/StreamDataView.tsx | 103 ++++++++++++++++++ .../StreamDataView12/index.ts | 3 + .../StreamDataView12/styles.module.scss | 94 ++++++++++++++++ .../StreamDataViewWrapper.tsx | 2 +- 5 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/StreamDataView.spec.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/StreamDataView.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/index.ts create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/styles.module.scss diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/StreamDataView.spec.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/StreamDataView.spec.tsx new file mode 100644 index 0000000000..0ef6e2105c --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/StreamDataView.spec.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import StreamDataView, { Props } from './StreamDataView' + +const mockedProps = mock() + +describe('StreamDataView', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/StreamDataView.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/StreamDataView.tsx new file mode 100644 index 0000000000..863c63eb1d --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/StreamDataView.tsx @@ -0,0 +1,103 @@ +import React, { useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { isNull } from 'lodash' +import cx from 'classnames' + +import { + fetchStreamEntries, + streamDataSelector, + streamSelector, +} from 'uiSrc/slices/browser/stream' +import VirtualTable from 'uiSrc/components/virtual-table/VirtualTable' +import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces' +import { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys' +import { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api' +import { SortOrder } from 'uiSrc/constants' +import { StreamEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' + +import styles from './styles.module.scss' + +const headerHeight = 60 +const rowHeight = 54 +const actionsWidth = 54 +const minColumnWidth = 190 +const noItemsMessageInEmptyStream = 'There are no Entries in the Stream.' +const noItemsMessageInRange = 'No results found.' + +interface IStreamEntry extends StreamEntryDto { + editing: boolean +} + +export interface Props { + data: IStreamEntry[] + columns: ITableColumn[] + onEditEntry: (entryId:string, editing: boolean) => void + onClosePopover: () => void + loadMoreItems: () => void + isFooterOpen?: boolean +} + +const StreamDataView = (props: Props) => { + const { data: entries = [], columns = [], onClosePopover, loadMoreItems, isFooterOpen } = props + const dispatch = useDispatch() + + const { loading } = useSelector(streamSelector) + const { + total, + firstEntry, + lastEntry, + } = useSelector(streamDataSelector) + const { name: key } = useSelector(selectedKeyDataSelector) ?? { name: '' } + + const [sortedColumnName, setSortedColumnName] = useState('id') + const [sortedColumnOrder, setSortedColumnOrder] = useState(SortOrder.DESC) + + const onChangeSorting = (column: any, order: SortOrder) => { + setSortedColumnName(column) + setSortedColumnOrder(order) + + dispatch(fetchStreamEntries(key, SCAN_COUNT_DEFAULT, order, false)) + } + + return ( + <> + +
+ {/*
+ +
*/} + +
+ + ) +} + +export default StreamDataView diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/index.ts b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/index.ts new file mode 100644 index 0000000000..bc8c479afd --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/index.ts @@ -0,0 +1,3 @@ +import StreamDataView from './StreamDataView' + +export default StreamDataView diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/styles.module.scss b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/styles.module.scss new file mode 100644 index 0000000000..0207f67bc1 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/styles.module.scss @@ -0,0 +1,94 @@ +.container { + display: flex; + flex: 1; + width: 100%; + padding-top: 3px; + background-color: var(--euiColorEmptyShade); + + :global { + .ReactVirtualized__Grid__innerScrollContainer { + .ReactVirtualized__Table__rowColumn { + border-right: 1px solid var(--tableDarkestBorderColor) !important; + + &:last-of-type, + &:nth-last-of-type(2) { + border-right: none !important; + } + } + + .ReactVirtualized__Table__row { + border-bottom: 1px solid var(--tableDarkestBorderColor) !important; + &:last-of-type { + border-bottom: none !important; + } + } + + & > div:hover { + background: var(--euiColorLightestShade); + + .value-table-actions { + background-color: var(--euiColorLightestShade) !important; + } + + .streamEntry { + color: var(--inputTextColor) !important; + } + + .streamEntryId { + color: var(--euiTextSubduedColor) !important; + } + } + } + + .ReactVirtualized__Table__headerRow { + border: none !important; + } + + .ReactVirtualized__Table__Grid { + border: 1px solid var(--tableDarkestBorderColor) !important; + } + } + + .cellHeader { + border: none !important; + } +} + +:global(.streamEntry) { + color: var(--euiTextSubduedColor) !important; + white-space: normal; + max-width: 100%; + word-break: break-all; +} + +:global(.streamEntryId) { + color: var(--euiColorMediumShade) !important; + display: flex; +} + +:global(.stream-entry-actions) { + margin-left: -5px; +} + +.actions, +.actionsHeader { + width: 54px; +} + +.actions { + :global(.value-table-actions) { + background-color: var(--euiColorEmptyShade) !important; + } +} + +.columnManager { + z-index: 11; + position: absolute; + right: 18px; + margin-top: 20px; + width: 40px; + button { + width: 40px; + background-color: var(--euiColorEmptyShade) !important; + } +} diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx index 0787fab750..2fec1d92a6 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx @@ -14,7 +14,7 @@ import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { keysSelector } from 'uiSrc/slices/browser/keys' import { StreamEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' -import StreamDataView from './StreamDataView' +import StreamDataView from './StreamDataView12' import styles from './StreamDataView/styles.module.scss' export interface IStreamEntry extends StreamEntryDto { From a4f40c4e7cf7c99584f269d837de3f9a2a2098ba Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Thu, 26 May 2022 11:02:51 +0400 Subject: [PATCH 08/20] rename folder --- .../StreamDataView12/StreamDataView.spec.tsx | 12 -- .../StreamDataView12/StreamDataView.tsx | 103 ------------------ .../StreamDataView12/index.ts | 3 - .../StreamDataView12/styles.module.scss | 94 ---------------- .../StreamDataViewWrapper.tsx | 2 +- 5 files changed, 1 insertion(+), 213 deletions(-) delete mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/StreamDataView.spec.tsx delete mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/StreamDataView.tsx delete mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/index.ts delete mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/styles.module.scss diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/StreamDataView.spec.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/StreamDataView.spec.tsx deleted file mode 100644 index 0ef6e2105c..0000000000 --- a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/StreamDataView.spec.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react' -import { instance, mock } from 'ts-mockito' -import { render } from 'uiSrc/utils/test-utils' -import StreamDataView, { Props } from './StreamDataView' - -const mockedProps = mock() - -describe('StreamDataView', () => { - it('should render', () => { - expect(render()).toBeTruthy() - }) -}) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/StreamDataView.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/StreamDataView.tsx deleted file mode 100644 index 863c63eb1d..0000000000 --- a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/StreamDataView.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import React, { useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { isNull } from 'lodash' -import cx from 'classnames' - -import { - fetchStreamEntries, - streamDataSelector, - streamSelector, -} from 'uiSrc/slices/browser/stream' -import VirtualTable from 'uiSrc/components/virtual-table/VirtualTable' -import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces' -import { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys' -import { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api' -import { SortOrder } from 'uiSrc/constants' -import { StreamEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' - -import styles from './styles.module.scss' - -const headerHeight = 60 -const rowHeight = 54 -const actionsWidth = 54 -const minColumnWidth = 190 -const noItemsMessageInEmptyStream = 'There are no Entries in the Stream.' -const noItemsMessageInRange = 'No results found.' - -interface IStreamEntry extends StreamEntryDto { - editing: boolean -} - -export interface Props { - data: IStreamEntry[] - columns: ITableColumn[] - onEditEntry: (entryId:string, editing: boolean) => void - onClosePopover: () => void - loadMoreItems: () => void - isFooterOpen?: boolean -} - -const StreamDataView = (props: Props) => { - const { data: entries = [], columns = [], onClosePopover, loadMoreItems, isFooterOpen } = props - const dispatch = useDispatch() - - const { loading } = useSelector(streamSelector) - const { - total, - firstEntry, - lastEntry, - } = useSelector(streamDataSelector) - const { name: key } = useSelector(selectedKeyDataSelector) ?? { name: '' } - - const [sortedColumnName, setSortedColumnName] = useState('id') - const [sortedColumnOrder, setSortedColumnOrder] = useState(SortOrder.DESC) - - const onChangeSorting = (column: any, order: SortOrder) => { - setSortedColumnName(column) - setSortedColumnOrder(order) - - dispatch(fetchStreamEntries(key, SCAN_COUNT_DEFAULT, order, false)) - } - - return ( - <> - -
- {/*
- -
*/} - -
- - ) -} - -export default StreamDataView diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/index.ts b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/index.ts deleted file mode 100644 index bc8c479afd..0000000000 --- a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import StreamDataView from './StreamDataView' - -export default StreamDataView diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/styles.module.scss b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/styles.module.scss deleted file mode 100644 index 0207f67bc1..0000000000 --- a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView12/styles.module.scss +++ /dev/null @@ -1,94 +0,0 @@ -.container { - display: flex; - flex: 1; - width: 100%; - padding-top: 3px; - background-color: var(--euiColorEmptyShade); - - :global { - .ReactVirtualized__Grid__innerScrollContainer { - .ReactVirtualized__Table__rowColumn { - border-right: 1px solid var(--tableDarkestBorderColor) !important; - - &:last-of-type, - &:nth-last-of-type(2) { - border-right: none !important; - } - } - - .ReactVirtualized__Table__row { - border-bottom: 1px solid var(--tableDarkestBorderColor) !important; - &:last-of-type { - border-bottom: none !important; - } - } - - & > div:hover { - background: var(--euiColorLightestShade); - - .value-table-actions { - background-color: var(--euiColorLightestShade) !important; - } - - .streamEntry { - color: var(--inputTextColor) !important; - } - - .streamEntryId { - color: var(--euiTextSubduedColor) !important; - } - } - } - - .ReactVirtualized__Table__headerRow { - border: none !important; - } - - .ReactVirtualized__Table__Grid { - border: 1px solid var(--tableDarkestBorderColor) !important; - } - } - - .cellHeader { - border: none !important; - } -} - -:global(.streamEntry) { - color: var(--euiTextSubduedColor) !important; - white-space: normal; - max-width: 100%; - word-break: break-all; -} - -:global(.streamEntryId) { - color: var(--euiColorMediumShade) !important; - display: flex; -} - -:global(.stream-entry-actions) { - margin-left: -5px; -} - -.actions, -.actionsHeader { - width: 54px; -} - -.actions { - :global(.value-table-actions) { - background-color: var(--euiColorEmptyShade) !important; - } -} - -.columnManager { - z-index: 11; - position: absolute; - right: 18px; - margin-top: 20px; - width: 40px; - button { - width: 40px; - background-color: var(--euiColorEmptyShade) !important; - } -} diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx index 2fec1d92a6..0787fab750 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx @@ -14,7 +14,7 @@ import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { keysSelector } from 'uiSrc/slices/browser/keys' import { StreamEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' -import StreamDataView from './StreamDataView12' +import StreamDataView from './StreamDataView' import styles from './StreamDataView/styles.module.scss' export interface IStreamEntry extends StreamEntryDto { From bab51acba01338c57ed8a42941526b07ea1f31b8 Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Thu, 26 May 2022 11:08:21 +0400 Subject: [PATCH 09/20] fix folder name --- .../StreamDataView/StreamDataView.spec.tsx | 12 ++ .../StreamDataView/StreamDataView.tsx | 103 ++++++++++++++++++ .../stream-data-view/StreamDataView/index.ts | 3 + .../StreamDataView/styles.module.scss | 94 ++++++++++++++++ 4 files changed, 212 insertions(+) create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView/StreamDataView.spec.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView/StreamDataView.tsx create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView/index.ts create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView/styles.module.scss diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView/StreamDataView.spec.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView/StreamDataView.spec.tsx new file mode 100644 index 0000000000..0ef6e2105c --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView/StreamDataView.spec.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import StreamDataView, { Props } from './StreamDataView' + +const mockedProps = mock() + +describe('StreamDataView', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView/StreamDataView.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView/StreamDataView.tsx new file mode 100644 index 0000000000..863c63eb1d --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView/StreamDataView.tsx @@ -0,0 +1,103 @@ +import React, { useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { isNull } from 'lodash' +import cx from 'classnames' + +import { + fetchStreamEntries, + streamDataSelector, + streamSelector, +} from 'uiSrc/slices/browser/stream' +import VirtualTable from 'uiSrc/components/virtual-table/VirtualTable' +import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces' +import { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys' +import { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api' +import { SortOrder } from 'uiSrc/constants' +import { StreamEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' + +import styles from './styles.module.scss' + +const headerHeight = 60 +const rowHeight = 54 +const actionsWidth = 54 +const minColumnWidth = 190 +const noItemsMessageInEmptyStream = 'There are no Entries in the Stream.' +const noItemsMessageInRange = 'No results found.' + +interface IStreamEntry extends StreamEntryDto { + editing: boolean +} + +export interface Props { + data: IStreamEntry[] + columns: ITableColumn[] + onEditEntry: (entryId:string, editing: boolean) => void + onClosePopover: () => void + loadMoreItems: () => void + isFooterOpen?: boolean +} + +const StreamDataView = (props: Props) => { + const { data: entries = [], columns = [], onClosePopover, loadMoreItems, isFooterOpen } = props + const dispatch = useDispatch() + + const { loading } = useSelector(streamSelector) + const { + total, + firstEntry, + lastEntry, + } = useSelector(streamDataSelector) + const { name: key } = useSelector(selectedKeyDataSelector) ?? { name: '' } + + const [sortedColumnName, setSortedColumnName] = useState('id') + const [sortedColumnOrder, setSortedColumnOrder] = useState(SortOrder.DESC) + + const onChangeSorting = (column: any, order: SortOrder) => { + setSortedColumnName(column) + setSortedColumnOrder(order) + + dispatch(fetchStreamEntries(key, SCAN_COUNT_DEFAULT, order, false)) + } + + return ( + <> + +
+ {/*
+ +
*/} + +
+ + ) +} + +export default StreamDataView diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView/index.ts b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView/index.ts new file mode 100644 index 0000000000..bc8c479afd --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView/index.ts @@ -0,0 +1,3 @@ +import StreamDataView from './StreamDataView' + +export default StreamDataView diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView/styles.module.scss b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView/styles.module.scss new file mode 100644 index 0000000000..0207f67bc1 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView/styles.module.scss @@ -0,0 +1,94 @@ +.container { + display: flex; + flex: 1; + width: 100%; + padding-top: 3px; + background-color: var(--euiColorEmptyShade); + + :global { + .ReactVirtualized__Grid__innerScrollContainer { + .ReactVirtualized__Table__rowColumn { + border-right: 1px solid var(--tableDarkestBorderColor) !important; + + &:last-of-type, + &:nth-last-of-type(2) { + border-right: none !important; + } + } + + .ReactVirtualized__Table__row { + border-bottom: 1px solid var(--tableDarkestBorderColor) !important; + &:last-of-type { + border-bottom: none !important; + } + } + + & > div:hover { + background: var(--euiColorLightestShade); + + .value-table-actions { + background-color: var(--euiColorLightestShade) !important; + } + + .streamEntry { + color: var(--inputTextColor) !important; + } + + .streamEntryId { + color: var(--euiTextSubduedColor) !important; + } + } + } + + .ReactVirtualized__Table__headerRow { + border: none !important; + } + + .ReactVirtualized__Table__Grid { + border: 1px solid var(--tableDarkestBorderColor) !important; + } + } + + .cellHeader { + border: none !important; + } +} + +:global(.streamEntry) { + color: var(--euiTextSubduedColor) !important; + white-space: normal; + max-width: 100%; + word-break: break-all; +} + +:global(.streamEntryId) { + color: var(--euiColorMediumShade) !important; + display: flex; +} + +:global(.stream-entry-actions) { + margin-left: -5px; +} + +.actions, +.actionsHeader { + width: 54px; +} + +.actions { + :global(.value-table-actions) { + background-color: var(--euiColorEmptyShade) !important; + } +} + +.columnManager { + z-index: 11; + position: absolute; + right: 18px; + margin-top: 20px; + width: 40px; + button { + width: 40px; + background-color: var(--euiColorEmptyShade) !important; + } +} From 94e012b9bf00bb82cb398063a8ad99217463bd39 Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Thu, 26 May 2022 11:13:45 +0400 Subject: [PATCH 10/20] delete wrong folder --- .../streamDataView/StreamDataView.spec.tsx | 12 -- .../streamDataView/StreamDataView.tsx | 103 ------------------ .../stream-data-view/streamDataView/index.ts | 3 - .../streamDataView/styles.module.scss | 94 ---------------- 4 files changed, 212 deletions(-) delete mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/StreamDataView.spec.tsx delete mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/StreamDataView.tsx delete mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/index.ts delete mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/styles.module.scss diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/StreamDataView.spec.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/StreamDataView.spec.tsx deleted file mode 100644 index 0ef6e2105c..0000000000 --- a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/StreamDataView.spec.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react' -import { instance, mock } from 'ts-mockito' -import { render } from 'uiSrc/utils/test-utils' -import StreamDataView, { Props } from './StreamDataView' - -const mockedProps = mock() - -describe('StreamDataView', () => { - it('should render', () => { - expect(render()).toBeTruthy() - }) -}) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/StreamDataView.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/StreamDataView.tsx deleted file mode 100644 index 863c63eb1d..0000000000 --- a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/StreamDataView.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import React, { useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { isNull } from 'lodash' -import cx from 'classnames' - -import { - fetchStreamEntries, - streamDataSelector, - streamSelector, -} from 'uiSrc/slices/browser/stream' -import VirtualTable from 'uiSrc/components/virtual-table/VirtualTable' -import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces' -import { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys' -import { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api' -import { SortOrder } from 'uiSrc/constants' -import { StreamEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' - -import styles from './styles.module.scss' - -const headerHeight = 60 -const rowHeight = 54 -const actionsWidth = 54 -const minColumnWidth = 190 -const noItemsMessageInEmptyStream = 'There are no Entries in the Stream.' -const noItemsMessageInRange = 'No results found.' - -interface IStreamEntry extends StreamEntryDto { - editing: boolean -} - -export interface Props { - data: IStreamEntry[] - columns: ITableColumn[] - onEditEntry: (entryId:string, editing: boolean) => void - onClosePopover: () => void - loadMoreItems: () => void - isFooterOpen?: boolean -} - -const StreamDataView = (props: Props) => { - const { data: entries = [], columns = [], onClosePopover, loadMoreItems, isFooterOpen } = props - const dispatch = useDispatch() - - const { loading } = useSelector(streamSelector) - const { - total, - firstEntry, - lastEntry, - } = useSelector(streamDataSelector) - const { name: key } = useSelector(selectedKeyDataSelector) ?? { name: '' } - - const [sortedColumnName, setSortedColumnName] = useState('id') - const [sortedColumnOrder, setSortedColumnOrder] = useState(SortOrder.DESC) - - const onChangeSorting = (column: any, order: SortOrder) => { - setSortedColumnName(column) - setSortedColumnOrder(order) - - dispatch(fetchStreamEntries(key, SCAN_COUNT_DEFAULT, order, false)) - } - - return ( - <> - -
- {/*
- -
*/} - -
- - ) -} - -export default StreamDataView diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/index.ts b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/index.ts deleted file mode 100644 index bc8c479afd..0000000000 --- a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import StreamDataView from './StreamDataView' - -export default StreamDataView diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/styles.module.scss b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/styles.module.scss deleted file mode 100644 index 0207f67bc1..0000000000 --- a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/streamDataView/styles.module.scss +++ /dev/null @@ -1,94 +0,0 @@ -.container { - display: flex; - flex: 1; - width: 100%; - padding-top: 3px; - background-color: var(--euiColorEmptyShade); - - :global { - .ReactVirtualized__Grid__innerScrollContainer { - .ReactVirtualized__Table__rowColumn { - border-right: 1px solid var(--tableDarkestBorderColor) !important; - - &:last-of-type, - &:nth-last-of-type(2) { - border-right: none !important; - } - } - - .ReactVirtualized__Table__row { - border-bottom: 1px solid var(--tableDarkestBorderColor) !important; - &:last-of-type { - border-bottom: none !important; - } - } - - & > div:hover { - background: var(--euiColorLightestShade); - - .value-table-actions { - background-color: var(--euiColorLightestShade) !important; - } - - .streamEntry { - color: var(--inputTextColor) !important; - } - - .streamEntryId { - color: var(--euiTextSubduedColor) !important; - } - } - } - - .ReactVirtualized__Table__headerRow { - border: none !important; - } - - .ReactVirtualized__Table__Grid { - border: 1px solid var(--tableDarkestBorderColor) !important; - } - } - - .cellHeader { - border: none !important; - } -} - -:global(.streamEntry) { - color: var(--euiTextSubduedColor) !important; - white-space: normal; - max-width: 100%; - word-break: break-all; -} - -:global(.streamEntryId) { - color: var(--euiColorMediumShade) !important; - display: flex; -} - -:global(.stream-entry-actions) { - margin-left: -5px; -} - -.actions, -.actionsHeader { - width: 54px; -} - -.actions { - :global(.value-table-actions) { - background-color: var(--euiColorEmptyShade) !important; - } -} - -.columnManager { - z-index: 11; - position: absolute; - right: 18px; - margin-top: 20px; - width: 40px; - button { - width: 40px; - background-color: var(--euiColorEmptyShade) !important; - } -} From d4d7bd375cb48d690bed94b96ef92bda015020ec Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Thu, 26 May 2022 11:29:35 +0400 Subject: [PATCH 11/20] fix errors --- redisinsight/ui/src/slices/browser/stream.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/redisinsight/ui/src/slices/browser/stream.ts b/redisinsight/ui/src/slices/browser/stream.ts index 098bf95a8d..2dde0dace7 100644 --- a/redisinsight/ui/src/slices/browser/stream.ts +++ b/redisinsight/ui/src/slices/browser/stream.ts @@ -169,10 +169,8 @@ const streamSlice = createSlice({ state.groups.selectedGroup = { ...state.groups.selectedGroup, - data: { - ...payload, - lastRefreshTime: Date.now() - } + lastRefreshTime: Date.now(), + data: payload, } }, @@ -183,10 +181,8 @@ const streamSlice = createSlice({ ...state.groups.selectedGroup, selectedConsumer: { ...state.groups.selectedGroup.selectedConsumer, - data: { - ...payload, - lastRefreshTime: Date.now() - } + lastRefreshTime: Date.now(), + data: payload, } } }, From f990fac184bad3762064beef172ffc178722f35b Mon Sep 17 00:00:00 2001 From: "Roman.Sergeenko" Date: Fri, 27 May 2022 09:39:13 +0400 Subject: [PATCH 12/20] #RI-2935 - base implementation of modify last entry id --- .../PopoverItemEditor.spec.tsx | 21 +++ .../popover-item-editor/PopoverItemEditor.tsx | 123 ++++++++++++++++++ .../components/popover-item-editor/index.ts | 3 + .../popover-item-editor/styles.module.scss | 7 + .../popover-delete/PopoverDelete.tsx | 3 +- .../groups-view/GroupsView/GroupsView.tsx | 1 - .../groups-view/GroupsView/styles.module.scss | 4 + .../groups-view/GroupsViewWrapper.tsx | 92 +++++++++---- redisinsight/ui/src/slices/browser/stream.ts | 36 +++++ 9 files changed, 265 insertions(+), 25 deletions(-) create mode 100644 redisinsight/ui/src/components/popover-item-editor/PopoverItemEditor.spec.tsx create mode 100644 redisinsight/ui/src/components/popover-item-editor/PopoverItemEditor.tsx create mode 100644 redisinsight/ui/src/components/popover-item-editor/index.ts create mode 100644 redisinsight/ui/src/components/popover-item-editor/styles.module.scss diff --git a/redisinsight/ui/src/components/popover-item-editor/PopoverItemEditor.spec.tsx b/redisinsight/ui/src/components/popover-item-editor/PopoverItemEditor.spec.tsx new file mode 100644 index 0000000000..6d3cb2cdef --- /dev/null +++ b/redisinsight/ui/src/components/popover-item-editor/PopoverItemEditor.spec.tsx @@ -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() + +describe('PopoverItemEditor', () => { + it('should render', () => { + expect( + render( + + <> + + ) + ).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/components/popover-item-editor/PopoverItemEditor.tsx b/redisinsight/ui/src/components/popover-item-editor/PopoverItemEditor.tsx new file mode 100644 index 0000000000..33781138ba --- /dev/null +++ b/redisinsight/ui/src/components/popover-item-editor/PopoverItemEditor.tsx @@ -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(false) + + useEffect(() => + // componentWillUnmount + () => { + declineOnUnmount && onDecline?.() + }, + []) + + const onFormSubmit = (e: FormEvent) => { + e.preventDefault() + handleApply() + } + + const handleApply = (): void => { + setIsPopoverOpen(false) + onApply() + } + + const handleDecline = () => { + setIsPopoverOpen(false) + onDecline?.() + } + + const handleButtonClick = (e: React.MouseEvent) => { + e.stopPropagation() + onOpen?.() + setIsPopoverOpen(true) + } + + const isDisabledApply = (): boolean => !!(isLoading || isDisabled) + + const button = ( + + ) + + return ( + e.stopPropagation()} + > + +
+ {children} +
+ + + handleDecline()} data-testid="cancel-btn"> + Cancel + + + + + + Save + + + +
+
+ ) +} + +export default PopoverItemEditor diff --git a/redisinsight/ui/src/components/popover-item-editor/index.ts b/redisinsight/ui/src/components/popover-item-editor/index.ts new file mode 100644 index 0000000000..e23e3abb72 --- /dev/null +++ b/redisinsight/ui/src/components/popover-item-editor/index.ts @@ -0,0 +1,3 @@ +import PopoverItemEditor from './PopoverItemEditor' + +export default PopoverItemEditor diff --git a/redisinsight/ui/src/components/popover-item-editor/styles.module.scss b/redisinsight/ui/src/components/popover-item-editor/styles.module.scss new file mode 100644 index 0000000000..761b0a4e2f --- /dev/null +++ b/redisinsight/ui/src/components/popover-item-editor/styles.module.scss @@ -0,0 +1,7 @@ +.content { + +} + +.footer { + margin-top: 6px !important; +} diff --git a/redisinsight/ui/src/pages/browser/components/popover-delete/PopoverDelete.tsx b/redisinsight/ui/src/pages/browser/components/popover-delete/PopoverDelete.tsx index 7b0dee2f43..755fbe5fc9 100644 --- a/redisinsight/ui/src/pages/browser/components/popover-delete/PopoverDelete.tsx +++ b/redisinsight/ui/src/pages/browser/components/popover-delete/PopoverDelete.tsx @@ -34,7 +34,8 @@ const PopoverDelete = (props: Props) => { testid = '', } = props - const onButtonClick = () => { + const onButtonClick = (e: React.MouseEvent) => { + e.stopPropagation() if (item + suffix !== deleting) { showPopover(item) handleButtonClick?.() diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/GroupsView.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/GroupsView.tsx index 86b58d8b27..bbb278578d 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/GroupsView.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/GroupsView.tsx @@ -28,7 +28,6 @@ interface IStreamEntry extends StreamEntryDto { export interface Props { data: IStreamEntry[] columns: ITableColumn[] - onEditGroup: (groupId:string, editing: boolean) => void onClosePopover: () => void onSelectGroup: ({ rowData }: { rowData: any }) => void isFooterOpen?: boolean diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/styles.module.scss b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/styles.module.scss index e9e8c44e93..223c27d966 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/styles.module.scss @@ -75,6 +75,10 @@ width: 54px; } +.editLastId { + margin-right: 4px; +} + .actions { :global(.value-table-actions) { background-color: var(--euiColorEmptyShade) !important; diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsViewWrapper.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsViewWrapper.tsx index d86a4c1acb..8e8b317c15 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsViewWrapper.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsViewWrapper.tsx @@ -1,14 +1,24 @@ -import { EuiText } from '@elastic/eui' +import { EuiFieldText, EuiIcon, EuiSpacer, EuiText, EuiToolTip } from '@elastic/eui' import React, { useCallback, useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' - -import { streamGroupsSelector, deleteStreamEntry, fetchConsumerGroups, setSelectedGroup, fetchConsumers, setStreamViewType } from 'uiSrc/slices/browser/stream' +import PopoverItemEditor from 'uiSrc/components/popover-item-editor' +import { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys' + +import { + streamGroupsSelector, + deleteStreamEntry, + fetchConsumerGroups, + 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 { getFormatTime } from 'uiSrc/utils/streamUtils' -import { TableCellTextAlignment } from 'uiSrc/constants' +import { KeyTypes, TableCellTextAlignment } from 'uiSrc/constants' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' -import { StreamEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' +import { ConsumerGroupDto, StreamEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' import { StreamViewType } from 'uiSrc/slices/interfaces/stream' import GroupsView from './GroupsView' @@ -20,7 +30,7 @@ export interface IStreamEntry extends StreamEntryDto { } const suffix = '_stream_groups' -const actionsWidth = 50 +const actionsWidth = 80 const minColumnWidth = 190 interface Props { @@ -32,18 +42,20 @@ const GroupsViewWrapper = (props: Props) => { data: loadedGroups = [], } = useSelector(streamGroupsSelector) const { id: instanceId, name: key = '' } = useSelector(connectedInstanceSelector) + const { name: selectedKey } = useSelector(selectedKeyDataSelector) const dispatch = useDispatch() - const [groups, setGroups] = useState([]) + const [groups, setGroups] = useState([]) const [deleting, setDeleting] = useState('') + const [editValue, setEditValue] = useState('') useEffect(() => { dispatch(fetchConsumerGroups()) }, []) useEffect(() => { - const streamGroups: IStreamEntry[] = loadedGroups?.map((item) => ({ + const streamGroups: ConsumerGroupDto[] = loadedGroups?.map((item) => ({ ...item, editing: false, })) @@ -78,16 +90,6 @@ const GroupsViewWrapper = (props: Props) => { // }) } - const handleEditGroup = (groupId = '', editing: boolean) => { - const newGroupsState = groups.map((item) => { - if (item.id === groupId) { - return { ...item, editing } - } - return item - }) - setGroups(newGroupsState) - } - const handleSelectGroup = ({ rowData }: { rowData: any }) => { dispatch(setSelectedGroup(rowData)) dispatch(fetchConsumers( @@ -96,6 +98,15 @@ const GroupsViewWrapper = (props: Props) => { )) } + const handleApplyEditId = (groupName: string) => { + const data = { + keyName: selectedKey, + name: groupName, + lastDeliveredId: editValue + } + dispatch(modifyLastDeliveredIdAction(data)) + } + const columns: ITableColumn[] = [ { @@ -135,7 +146,7 @@ const GroupsViewWrapper = (props: Props) => { isSortable: true, className: styles.cell, headerClassName: styles.cellHeader, - render: function Id(_name: string, { lastDeliveredId: id }: StreamEntryDto) { + render: function Id(_name: string, { lastDeliveredId: id }: ConsumerGroupDto) { const timestamp = id?.split('-')?.[0] return (
@@ -161,9 +172,45 @@ const GroupsViewWrapper = (props: Props) => { absoluteWidth: actionsWidth, maxWidth: actionsWidth, minWidth: actionsWidth, - render: function Actions(_act: any, { id }: StreamEntryDto) { + render: function Actions(_act: any, { lastDeliveredId, name }: ConsumerGroupDto) { return (
+ setEditValue(lastDeliveredId)} + onApply={() => handleApplyEditId(name)} + className={styles.editLastId} + > + setEditValue(e.target.value)} + append={( + + Specify the ID of the last delivered entry in the stream from the new group's perspective. + + Otherwise, $ represents the ID of the last entry in the stream,  + 0 fetches the entire stream from the beginning. + + )} + > + + + )} + style={{ width: 240 }} + autoComplete="off" + data-testid="id-field" + /> + @@ -172,13 +219,13 @@ const GroupsViewWrapper = (props: Props) => { {key} )} - item={id} + item={lastDeliveredId} suffix={suffix} deleting={deleting} closePopover={closePopover} updateLoading={false} showPopover={showPopover} - testid={`remove-groups-button-${id}`} + testid={`remove-groups-button-${lastDeliveredId}`} handleDeleteItem={handleDeleteGroup} handleButtonClick={handleRemoveIconClick} /> @@ -193,7 +240,6 @@ const GroupsViewWrapper = (props: Props) => { void, + onFailed?: () => void +) { + return async (dispatch: AppDispatch, stateInit: () => RootState) => { + //dispatch(addNewEntries()) + + try { + const state = stateInit() + const keyName = state.browser.keys.selectedKey.data?.name + const { status } = await apiService.patch( + getUrl( + state.connections.instances.connectedInstance?.id, + ApiEndpoints.STREAMS_CONSUMER_GROUPS + ), + data + ) + + if (isStatusSuccessful(status)) { + //dispatch(addNewEntriesSuccess()) + dispatch(fetchConsumerGroups(false)) + keyName && dispatch(refreshKeyInfoAction(keyName)) + onSuccess?.() + } + } catch (_err) { + const error = _err as AxiosError + const errorMessage = getApiErrorMessage(error) + dispatch(addErrorNotification(error)) + //dispatch(addNewEntriesFailure(errorMessage)) + onFailed?.() + } + } +} From e9f81c9896af153da24155d66009d28f476dfdf4 Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Fri, 27 May 2022 09:43:07 +0400 Subject: [PATCH 13/20] #RI-2932 - Show Consumer Groups #RI-2766 - Show Consumer Information --- package.json | 1 - .../components/range-filter/RangeFilter.tsx | 4 + .../components/auto-refresh/AutoRefresh.tsx | 12 +- .../key-details/KeyDetailsWrapper.tsx | 26 +- .../StreamDetailsWrapper.spec.tsx | 12 +- .../stream-details/StreamDetailsWrapper.tsx | 13 +- .../ConsumersView/ConsumersView.spec.tsx | 16 +- .../ConsumersView/ConsumersView.tsx | 22 +- .../consumers-view/ConsumersView/index.ts | 6 +- .../ConsumersView/styles.module.scss | 70 --- .../ConsumersViewWrapper.spec.tsx | 70 ++- .../consumers-view/ConsumersViewWrapper.tsx | 29 +- .../stream-details/consumers-view/index.ts | 2 + .../GroupsView/GroupsView.spec.tsx | 22 +- .../groups-view/GroupsView/GroupsView.tsx | 36 +- .../groups-view/GroupsView/index.ts | 2 + .../groups-view/GroupsView/styles.module.scss | 70 +-- .../groups-view/GroupsViewWrapper.spec.tsx | 76 ++- .../groups-view/GroupsViewWrapper.tsx | 50 +- .../MessagesView/MessagesView.spec.tsx | 18 +- .../MessagesView/MessagesView.tsx | 18 +- .../messages-view/MessagesView/index.ts | 6 +- .../MessagesView/styles.module.scss | 70 --- .../MessagesViewWrapper.spec.tsx | 63 ++- .../messages-view/MessagesViewWrapper.tsx | 33 +- .../stream-details/messages-view/index.ts | 2 + .../StreamDataView/StreamDataView.tsx | 2 +- .../StreamDataView/styles.module.scss | 70 --- .../StreamDataViewWrapper.spec.tsx | 8 +- .../StreamDataViewWrapper.tsx | 29 +- .../stream-tabs/StreamTabs.spec.tsx | 9 + .../stream-details/stream-tabs/StreamTabs.tsx | 49 +- .../stream-details/styles.module.scss | 70 +++ redisinsight/ui/src/slices/browser/keys.ts | 5 +- redisinsight/ui/src/slices/browser/stream.ts | 33 +- .../ui/src/slices/interfaces/stream.ts | 6 +- .../src/slices/tests/browser/stream.spec.ts | 524 +++++++++++++++++- .../slices/tests/workbench/wb-results.spec.ts | 2 +- .../ui/src/utils/tests/monacoUtils.spec.ts | 2 +- yarn.lock | 28 +- 40 files changed, 1091 insertions(+), 495 deletions(-) create mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.spec.tsx diff --git a/package.json b/package.json index 6384a0ae06..e802541fd4 100644 --- a/package.json +++ b/package.json @@ -202,7 +202,6 @@ "opencollective-postinstall": "^2.0.3", "react-hot-loader": "^4.13.0", "react-refresh": "^0.9.0", - "react-test-renderer": "^17.0.1", "redux-mock-store": "^1.5.4", "regenerator-runtime": "^0.13.5", "rimraf": "^3.0.2", diff --git a/redisinsight/ui/src/components/range-filter/RangeFilter.tsx b/redisinsight/ui/src/components/range-filter/RangeFilter.tsx index 173d06b279..4922c81e9b 100644 --- a/redisinsight/ui/src/components/range-filter/RangeFilter.tsx +++ b/redisinsight/ui/src/components/range-filter/RangeFilter.tsx @@ -12,6 +12,7 @@ export interface Props { min: number start: number end: number + disabled?: boolean handleChangeStart: (value: number, shouldSentEventTelemetry: boolean) => void handleChangeEnd: (value: number, shouldSentEventTelemetry: boolean) => void handleUpdateRangeMax: (value: number) => void @@ -33,6 +34,7 @@ const RangeFilter = (props: Props) => { min, start, end, + disabled = false, handleChangeStart, handleChangeEnd, handleUpdateRangeMax, @@ -152,6 +154,7 @@ const RangeFilter = (props: Props) => { max={max} value={startVal} ref={minValRef} + disabled={disabled} onChange={onChangeStart} onMouseUp={onMouseUpStart} className={cx(styles.thumb, styles.thumbZindex3)} @@ -163,6 +166,7 @@ const RangeFilter = (props: Props) => { max={max} value={endVal} ref={maxValRef} + disabled={disabled} onChange={onChangeEnd} onMouseUp={onMouseUpEnd} className={cx(styles.thumb, styles.thumbZindex4)} diff --git a/redisinsight/ui/src/pages/browser/components/auto-refresh/AutoRefresh.tsx b/redisinsight/ui/src/pages/browser/components/auto-refresh/AutoRefresh.tsx index 7c22a8c633..5da6be54a5 100644 --- a/redisinsight/ui/src/pages/browser/components/auto-refresh/AutoRefresh.tsx +++ b/redisinsight/ui/src/pages/browser/components/auto-refresh/AutoRefresh.tsx @@ -29,7 +29,7 @@ export interface Props { testid?: string containerClassName?: string turnOffAutoRefresh?: boolean - onRefresh: (enableAutoRefresh?: boolean) => void + onRefresh: (enableAutoRefresh: boolean) => void onEnableAutoRefresh?: (enableAutoRefresh: boolean, refreshRate: string) => void onChangeAutoRefreshRate?: (enableAutoRefresh: boolean, refreshRate: string) => void } @@ -49,7 +49,7 @@ const AutoRefresh = ({ onChangeAutoRefreshRate, }: Props) => { let intervalText: NodeJS.Timeout - let timeoutRefresh: NodeJS.Timeout + let intervalRefresh: NodeJS.Timeout const [refreshMessage, setRefreshMessage] = useState(NOW) const [isPopoverOpen, setIsPopoverOpen] = useState(false) @@ -74,7 +74,7 @@ const AutoRefresh = ({ useEffect(() => { if (turnOffAutoRefresh && enableAutoRefresh) { setEnableAutoRefresh(false) - clearInterval(timeoutRefresh) + clearInterval(intervalRefresh) } }, [turnOffAutoRefresh]) @@ -96,20 +96,20 @@ const AutoRefresh = ({ updateLastRefresh() if (enableAutoRefresh && !loading) { - timeoutRefresh = setInterval(() => { + intervalRefresh = setInterval(() => { if (document.hidden) return handleRefresh() }, +refreshRate * 1_000) } else { - clearInterval(timeoutRefresh) + clearInterval(intervalRefresh) } if (enableAutoRefresh) { updateAutoRefreshText(refreshRate) } - return () => clearInterval(timeoutRefresh) + return () => clearInterval(intervalRefresh) }, [enableAutoRefresh, refreshRate, loading, lastRefreshTime]) const getLastRefreshDelta = (time:Nullable) => (Date.now() - (time || 0)) / 1_000 diff --git a/redisinsight/ui/src/pages/browser/components/key-details/KeyDetailsWrapper.tsx b/redisinsight/ui/src/pages/browser/components/key-details/KeyDetailsWrapper.tsx index 691fee7500..95cdc35e92 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details/KeyDetailsWrapper.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-details/KeyDetailsWrapper.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from 'react' -import { useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { deleteKeyAction, editKey, @@ -15,7 +15,14 @@ import { fetchString, resetStringValue } from 'uiSrc/slices/browser/string' import { refreshSetMembersAction } from 'uiSrc/slices/browser/set' import { refreshListElementsAction } from 'uiSrc/slices/browser/list' import { fetchReJSON } from 'uiSrc/slices/browser/rejson' -import { refreshStreamEntries } from 'uiSrc/slices/browser/stream' +import { + fetchConsumerGroups, + fetchConsumerMessages, + fetchConsumers, + refreshStreamEntries, + streamSelector, +} from 'uiSrc/slices/browser/stream' +import { StreamViewType } from 'uiSrc/slices/interfaces/stream' import KeyDetails from './KeyDetails/KeyDetails' export interface Props { @@ -39,6 +46,8 @@ const KeyDetailsWrapper = (props: Props) => { keyProp } = props + const { viewType: streamViewType } = useSelector(streamSelector) + const dispatch = useDispatch() useEffect(() => { @@ -90,7 +99,18 @@ const KeyDetailsWrapper = (props: Props) => { break } case KeyTypes.Stream: { - dispatch(refreshStreamEntries(key, resetData)) + if (streamViewType === StreamViewType.Data) { + dispatch(refreshStreamEntries(key, resetData)) + } + if (streamViewType === StreamViewType.Groups) { + dispatch(fetchConsumerGroups(resetData)) + } + if (streamViewType === StreamViewType.Consumers) { + dispatch(fetchConsumers(resetData)) + } + if (streamViewType === StreamViewType.Messages) { + dispatch(fetchConsumerMessages(resetData)) + } break } default: diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetailsWrapper.spec.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetailsWrapper.spec.tsx index efaa276284..237a80099a 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetailsWrapper.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetailsWrapper.spec.tsx @@ -1,12 +1,18 @@ import React from 'react' import { instance, mock } from 'ts-mockito' -import { render } from 'uiSrc/utils/test-utils' -import StreamDataViewWrapper, { Props } from './StreamDetailsWrapper' +import { render, screen } from 'uiSrc/utils/test-utils' +import StreamDetailsWrapper, { Props } from './StreamDetailsWrapper' const mockedProps = mock() describe('StreamDetailsWrapper', () => { it('should render', () => { - expect(render()).toBeTruthy() + expect(render()).toBeTruthy() + }) + + it('should render Stream Data container', () => { + render() + + expect(screen.getByTestId('stream-entries-container')).toBeInTheDocument() }) }) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetailsWrapper.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetailsWrapper.tsx index 90a764fc75..c3a83d14e8 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetailsWrapper.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetailsWrapper.tsx @@ -15,15 +15,15 @@ import { fetchStreamEntries } from 'uiSrc/slices/browser/stream' import { StreamViewType } from 'uiSrc/slices/interfaces/stream' - import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { getNextId, getTimestampFromId } from 'uiSrc/utils/streamUtils' import { SortOrder } from 'uiSrc/constants' import { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api' import { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { GetStreamEntriesResponse } from 'apiSrc/modules/browser/dto/stream.dto' import RangeFilter from 'uiSrc/components/range-filter' +import { GetStreamEntriesResponse } from 'apiSrc/modules/browser/dto/stream.dto' + import ConsumersViewWrapper from './consumers-view' import GroupsViewWrapper from './groups-view' import MessagesViewWrapper from './messages-view' @@ -40,11 +40,7 @@ const StreamDetailsWrapper = (props: Props) => { const { viewType, loading, sortOrder: entryColumnSortOrder } = useSelector(streamSelector) const { loading: loadingGroups } = useSelector(streamGroupsSelector) const { start, end } = useSelector(streamRangeSelector) - const { - firstEntry, - lastEntry, - entries, - } = useSelector(streamDataSelector) + const { firstEntry, lastEntry, entries, } = useSelector(streamDataSelector) const { name: key } = useSelector(selectedKeyDataSelector) ?? { name: '' } const { id: instanceId } = useSelector(connectedInstanceSelector) @@ -80,7 +76,7 @@ const StreamDetailsWrapper = (props: Props) => { }, [lastEntryTimeStamp]) const loadMoreItems = () => { - const lastLoadedEntryId = last(entries)?.id + const lastLoadedEntryId = last(entries)?.id ?? '' const lastLoadedEntryTimeStamp = getTimestampFromId(lastLoadedEntryId) const lastRangeEntryTimestamp = end ? parseInt(end, 10) : getTimestampFromId(lastEntry?.id) @@ -189,6 +185,7 @@ const StreamDetailsWrapper = (props: Props) => { )} {shouldFilterRender ? ( () +const mockConsumers: ConsumerDto[] = [{ + name: 'test', + idle: 123, + pending: 321, +}, { + name: 'test2', + idle: 13, + pending: 31, +}] -describe('GroupsView', () => { +describe('ConsumersView', () => { it('should render', () => { - expect(render()).toBeTruthy() + expect(render()).toBeTruthy() }) }) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersView/ConsumersView.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersView/ConsumersView.tsx index f81fba3286..f599a81633 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersView/ConsumersView.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersView/ConsumersView.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import { useSelector } from 'react-redux' import cx from 'classnames' import { orderBy } from 'lodash' @@ -16,8 +16,6 @@ import styles from './styles.module.scss' const headerHeight = 60 const rowHeight = 54 -const actionsWidth = 54 -const minColumnWidth = 190 const noItemsMessageString = 'Your Consumer Group has no Consumers available.' export interface Props { @@ -36,13 +34,17 @@ const ConsumersView = (props: Props) => { const [consumers, setConsumers] = useState(data) const [sortedColumnName, setSortedColumnName] = useState('name') - const [sortedColumnOrder, setSortedColumnOrder] = useState(SortOrder.DESC) + const [sortedColumnOrder, setSortedColumnOrder] = useState(SortOrder.ASC) + + useEffect(() => { + setConsumers(orderBy(data, sortedColumnName, sortedColumnOrder?.toLowerCase())) + }, [data]) const onChangeSorting = (column: any, order: SortOrder) => { setSortedColumnName(column) setSortedColumnOrder(order) - setConsumers(orderBy(consumers, 'name', order?.toLowerCase())) + setConsumers(orderBy(consumers, column, order?.toLowerCase())) } return ( @@ -54,24 +56,24 @@ const ConsumersView = (props: Props) => { styles.container, { footerOpened: isFooterOpen } )} - data-test-id="stream-consumers-container" + data-testid="stream-consumers-container" > div:hover { - background: var(--euiColorLightestShade); - - .value-table-actions { - background-color: var(--euiColorLightestShade) !important; - } - - .streamEntry { - color: var(--inputTextColor) !important; - } - - .streamEntryId { - color: var(--euiTextSubduedColor) !important; - } - } - } - - .ReactVirtualized__Table__headerRow { - border: none !important; - } - - .ReactVirtualized__Table__Grid { - border: 1px solid var(--tableDarkestBorderColor) !important; - } - } - - .cellHeader { - border: none !important; - } -} - -:global(.streamEntry) { - color: var(--euiTextSubduedColor) !important; - white-space: normal; - max-width: 100%; - word-break: break-all; -} - -:global(.streamEntryId) { - color: var(--euiColorMediumShade) !important; - display: flex; -} - -:global(.stream-entry-actions) { - margin-left: -5px; } .actions, .actionsHeader { width: 54px; } - -.actions { - :global(.value-table-actions) { - background-color: var(--euiColorEmptyShade) !important; - } -} diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersViewWrapper.spec.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersViewWrapper.spec.tsx index 4e49318fb4..2dc6f23b15 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersViewWrapper.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersViewWrapper.spec.tsx @@ -1,12 +1,80 @@ import React from 'react' import { instance, mock } from 'ts-mockito' -import { render } from 'uiSrc/utils/test-utils' +import { cloneDeep } from 'lodash' +import { cleanup, fireEvent, mockedStore, render, screen } from 'uiSrc/utils/test-utils' +import { loadConsumerGroups, setSelectedConsumer } from 'uiSrc/slices/browser/stream' +import VirtualTable from 'uiSrc/components/virtual-table/VirtualTable' +import { ConsumerDto } from 'apiSrc/modules/browser/dto/stream.dto' +import ConsumersView, { Props as ConsumersViewProps } from './ConsumersView' import ConsumersViewWrapper, { Props } from './ConsumersViewWrapper' const mockedProps = mock() +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +jest.mock('./ConsumersView', () => ({ + __esModule: true, + namedExport: jest.fn(), + default: jest.fn(), +})) + +const mockConsumerName = 'group' +const mockConsumers: ConsumerDto[] = [{ + name: 'test', + idle: 123, + pending: 321, +}, { + name: 'test2', + idle: 13, + pending: 31, +}] + +const mockConsumersView = (props: ConsumersViewProps) => ( +
+ + + +
+) + describe('ConsumersViewWrapper', () => { + beforeAll(() => { + ConsumersView.mockImplementation(mockConsumersView) + }) + it('should render', () => { expect(render()).toBeTruthy() }) + + it('should render Consumers container', () => { + render() + + expect(screen.getByTestId('stream-consumers-container')).toBeInTheDocument() + }) + + it('should select Consumer', () => { + render() + + const afterRenderActions = [...store.getActions()] + + fireEvent.click(screen.getByTestId('select-consumer-btn')) + + expect(store.getActions()).toEqual([...afterRenderActions, setSelectedConsumer(), loadConsumerGroups(false)]) + }) }) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersViewWrapper.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersViewWrapper.tsx index 79667bcbb8..b5f15f4df4 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersViewWrapper.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/consumers-view/ConsumersViewWrapper.tsx @@ -10,10 +10,11 @@ import { } from 'uiSrc/slices/browser/stream' import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces' import PopoverDelete from 'uiSrc/pages/browser/components/popover-delete/PopoverDelete' -import { TableCellTextAlignment } from 'uiSrc/constants' +import { TableCellAlignment, TableCellTextAlignment } from 'uiSrc/constants' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { StreamViewType } from 'uiSrc/slices/interfaces/stream' import { numberWithSpaces } from 'uiSrc/utils/numbers' +import { updateSelectedKeyRefreshTime } from 'uiSrc/slices/browser/keys' import { ConsumerDto } from 'apiSrc/modules/browser/dto/stream.dto' import ConsumersView from './ConsumersView' @@ -22,23 +23,26 @@ import styles from './ConsumersView/styles.module.scss' const suffix = '_stream_consumer' const actionsWidth = 50 -const minColumnWidth = 190 -interface Props { +export interface Props { isFooterOpen: boolean } const ConsumersViewWrapper = (props: Props) => { + const { name: key = '' } = useSelector(connectedInstanceSelector) const { + lastRefreshTime, data: loadedConsumers = [], } = useSelector(selectedGroupSelector) ?? {} - const { id: instanceId, name: key = '' } = useSelector(connectedInstanceSelector) const dispatch = useDispatch() - const [consumers, setConsumers] = useState(loadedConsumers) const [deleting, setDeleting] = useState('') + useEffect(() => { + dispatch(updateSelectedKeyRefreshTime(lastRefreshTime)) + }, []) + const closePopover = useCallback(() => { setDeleting('') }, []) @@ -79,32 +83,35 @@ const ConsumersViewWrapper = (props: Props) => { { id: 'name', label: 'Consumer Name', - // minWidth: 180, + relativeWidth: 0.59, truncateText: true, isSortable: true, + headerClassName: 'streamItemHeader', }, { id: 'pending', label: 'Pending', minWidth: 106, - absoluteWidth: 106, + relativeWidth: 0.12, truncateText: true, isSortable: true, + headerClassName: 'streamItemHeader', }, { id: 'idle', label: 'Idle time, ms', - absoluteWidth: 190, minWidth: 190, + relativeWidth: 0.27, isSortable: true, + alignment: TableCellAlignment.Right, className: styles.cell, - headerClassName: styles.cellHeader, + headerClassName: 'streamItemHeader', render: (cellData: number) => numberWithSpaces(cellData), }, { id: 'actions', label: '', - headerClassName: styles.actionsHeader, + headerClassName: 'streamItemHeader', textAlignment: TableCellTextAlignment.Left, absoluteWidth: actionsWidth, maxWidth: actionsWidth, @@ -139,7 +146,7 @@ const ConsumersViewWrapper = (props: Props) => { return ( <> () +const mockGroups: IConsumerGroup[] = [{ + name: 'test', + consumers: 123, + pending: 321, + smallestPendingId: '123', + greatestPendingId: '123', + lastDeliveredId: '123', + editing: false, +}, { + name: 'test2', + consumers: 13, + pending: 31, + smallestPendingId: '3', + greatestPendingId: '23', + lastDeliveredId: '12', + editing: false, +}] + describe('GroupsView', () => { it('should render', () => { - expect(render()).toBeTruthy() + expect(render()).toBeTruthy() }) }) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/GroupsView.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/GroupsView.tsx index a3c0adfb9e..05f6afa0ab 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/GroupsView.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/GroupsView.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import { useSelector } from 'react-redux' import cx from 'classnames' import { orderBy } from 'lodash' @@ -16,11 +16,9 @@ import styles from './styles.module.scss' const headerHeight = 60 const rowHeight = 54 -const actionsWidth = 54 -const minColumnWidth = 190 const noItemsMessageString = 'Your Key has no Consumer Groups available.' -interface IConsumerGroup extends ConsumerGroupDto { +export interface IConsumerGroup extends ConsumerGroupDto { editing: boolean } @@ -39,16 +37,22 @@ const ConsumerGroups = (props: Props) => { const { loading } = useSelector(streamGroupsSelector) const { name: key = '' } = useSelector(selectedKeyDataSelector) ?? { } - const [groups, setGroups] = useState(data) + const [groups, setGroups] = useState([]) const [sortedColumnName, setSortedColumnName] = useState('name') - const [sortedColumnOrder, setSortedColumnOrder] = useState(SortOrder.DESC) + const [sortedColumnOrder, setSortedColumnOrder] = useState(SortOrder.ASC) - const onChangeSorting = (column: any, order: SortOrder) => { - setSortedColumnName(column) - setSortedColumnOrder(order) + useEffect(() => { + setGroups(orderBy(data, sortedColumnName, sortedColumnOrder?.toLowerCase())) + }, [data]) - setGroups(orderBy(groups, 'name', order?.toLowerCase())) - } + const onChangeSorting = useCallback( + (column: any, order: SortOrder) => { + setSortedColumnName(column) + setSortedColumnOrder(order) + + setGroups(orderBy(data, column, order?.toLowerCase())) + }, [groups] + ) return ( <> @@ -59,24 +63,24 @@ const ConsumerGroups = (props: Props) => { styles.container, { footerOpened: isFooterOpen } )} - data-test-id="stream-groups-container" + data-testid="stream-groups-container" > div:hover { - background: var(--euiColorLightestShade); - - .value-table-actions { - background-color: var(--euiColorLightestShade) !important; - } - - .streamEntry { - color: var(--inputTextColor) !important; - } - - .streamEntryId { - color: var(--euiTextSubduedColor) !important; - } - } - } - - .ReactVirtualized__Table__headerRow { - border: none !important; - } - - .ReactVirtualized__Table__Grid { - border: 1px solid var(--tableDarkestBorderColor) !important; - } - } - - .cellHeader { - border: none !important; - } -} - -:global(.streamEntry) { - color: var(--euiTextSubduedColor) !important; - white-space: normal; - max-width: 100%; - word-break: break-all; -} - -:global(.streamEntryId) { - color: var(--euiColorMediumShade) !important; - display: flex; -} - -:global(.stream-entry-actions) { - margin-left: -5px; } .actions, @@ -75,8 +11,6 @@ width: 54px; } -.actions { - :global(.value-table-actions) { - background-color: var(--euiColorEmptyShade) !important; - } +.tooltip { + min-width: 325px; } diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsViewWrapper.spec.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsViewWrapper.spec.tsx index 0e2908c5b8..4bc50c1dd3 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsViewWrapper.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsViewWrapper.spec.tsx @@ -1,12 +1,86 @@ import React from 'react' import { instance, mock } from 'ts-mockito' -import { render } from 'uiSrc/utils/test-utils' +import { cloneDeep } from 'lodash' +import { cleanup, fireEvent, mockedStore, render, screen } from 'uiSrc/utils/test-utils' +import { loadConsumerGroups, setSelectedGroup } from 'uiSrc/slices/browser/stream' +import VirtualTable from 'uiSrc/components/virtual-table/VirtualTable' +import { ConsumerGroupDto } from 'apiSrc/modules/browser/dto/stream.dto' +import GroupsView, { Props as GroupsViewProps } from './GroupsView' import GroupsViewWrapper, { Props } from './GroupsViewWrapper' const mockedProps = mock() +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +jest.mock('./GroupsView', () => ({ + __esModule: true, + namedExport: jest.fn(), + default: jest.fn(), +})) + +const mockGroupName = 'group' +const mockGroups: ConsumerGroupDto[] = [{ + name: 'test', + consumers: 123, + pending: 321, + smallestPendingId: '123', + greatestPendingId: '123', + lastDeliveredId: '123' +}, { + name: 'test2', + consumers: 13, + pending: 31, + smallestPendingId: '3', + greatestPendingId: '23', + lastDeliveredId: '12' +}] + +const mockGroupsView = (props: GroupsViewProps) => ( +
+ + + +
+) + describe('GroupsViewWrapper', () => { + beforeAll(() => { + GroupsView.mockImplementation(mockGroupsView) + }) + it('should render', () => { expect(render()).toBeTruthy() }) + + it('should render Groups container', () => { + render() + + expect(screen.getByTestId('stream-groups-container')).toBeInTheDocument() + }) + + it('should select Group', () => { + render() + + const afterRenderActions = [...store.getActions()] + + fireEvent.click(screen.getByTestId('select-group-btn')) + + expect(store.getActions()).toEqual([...afterRenderActions, setSelectedGroup(), loadConsumerGroups(false)]) + }) }) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsViewWrapper.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsViewWrapper.tsx index 0a3d58c4cd..65252b3dd5 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsViewWrapper.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsViewWrapper.tsx @@ -4,7 +4,6 @@ import { useDispatch, useSelector } from 'react-redux' import { streamGroupsSelector, - deleteStreamEntry, setSelectedGroup, fetchConsumers, setStreamViewType, @@ -14,9 +13,10 @@ import PopoverDelete from 'uiSrc/pages/browser/components/popover-delete/Popover import { getFormatTime } from 'uiSrc/utils/streamUtils' import { 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 { StreamViewType } from 'uiSrc/slices/interfaces/stream' import GroupsView from './GroupsView' import styles from './GroupsView/styles.module.scss' @@ -27,17 +27,17 @@ export interface IConsumerGroup extends ConsumerGroupDto { const suffix = '_stream_group' const actionsWidth = 50 -const minColumnWidth = 190 -interface Props { +export interface Props { isFooterOpen: boolean } const GroupsViewWrapper = (props: Props) => { const { + lastRefreshTime, data: loadedGroups = [], } = useSelector(streamGroupsSelector) - const { id: instanceId, name: key = '' } = useSelector(connectedInstanceSelector) + const { name: key = '' } = useSelector(connectedInstanceSelector) const dispatch = useDispatch() @@ -45,12 +45,16 @@ const GroupsViewWrapper = (props: Props) => { const [deleting, setDeleting] = useState('') useEffect(() => { - const streamGroups: IConsumerGroup[] = loadedGroups?.map((item) => ({ + dispatch(updateSelectedKeyRefreshTime(lastRefreshTime)) + }, [lastRefreshTime]) + + useEffect(() => { + const streamItem: IConsumerGroup[] = loadedGroups?.map((item) => ({ ...item, editing: false, })) - setGroups(streamGroups) + setGroups(streamItem) }, [loadedGroups, deleting]) const closePopover = useCallback(() => { @@ -61,7 +65,7 @@ const GroupsViewWrapper = (props: Props) => { setDeleting(`${groupName + suffix}`) }, []) - const handleDeleteGroup = (groupName = '') => { + const handleDeleteGroup = () => { // dispatch(deleteStreamEntry(key, [groupName])) closePopover() } @@ -103,35 +107,33 @@ const GroupsViewWrapper = (props: Props) => { { id: 'name', label: 'Group Name', - // minWidth: 180, truncateText: true, isSortable: true, - // render: (cellData: ConnectionType) => - // capitalize(cellData), + relativeWidth: 0.44, + headerClassName: 'streamItemHeader', }, { id: 'consumers', label: 'Consumers', minWidth: 130, - absoluteWidth: 130, + relativeWidth: 0.15, truncateText: true, isSortable: true, - // render: (cellData: ConnectionType) => - // capitalize(cellData), + headerClassName: 'streamItemHeader', }, { id: 'pending', label: 'Pending', minWidth: 106, - absoluteWidth: 106, + relativeWidth: 0.12, isSortable: true, className: styles.cell, - headerClassName: styles.cellHeader, + headerClassName: 'streamItemHeader', render: function P(_name: string, { pending, greatestPendingId, smallestPendingId, name }: ConsumerGroupDto) { const smallestTimestamp = smallestPendingId?.split('-')?.[0] const greatestTimestamp = greatestPendingId?.split('-')?.[0] - const tooltipContent = `${getFormatTime(smallestTimestamp)}-${getFormatTime(greatestTimestamp)}` + const tooltipContent = `${getFormatTime(smallestTimestamp)} – ${getFormatTime(greatestTimestamp)}` return (
@@ -155,22 +157,22 @@ const GroupsViewWrapper = (props: Props) => { { id: 'lastDeliveredId', label: 'Last Delivered ID', - absoluteWidth: 190, + relativeWidth: 0.25, minWidth: 190, isSortable: true, className: styles.cell, - headerClassName: styles.cellHeader, + headerClassName: 'streamItemHeader', render: function Id(_name: string, { lastDeliveredId: id }: ConsumerGroupDto) { const timestamp = id?.split('-')?.[0] return (
-
+
{getFormatTime(timestamp)}
-
+
{id}
@@ -186,7 +188,7 @@ const GroupsViewWrapper = (props: Props) => { absoluteWidth: actionsWidth, maxWidth: actionsWidth, minWidth: actionsWidth, - render: function Actions(_act: any, { id }: ConsumerGroupDto) { + render: function Actions(_act: any, { name }: ConsumerGroupDto) { return (
{ {key} )} - item={id} + item={name} suffix={suffix} deleting={deleting} closePopover={closePopover} updateLoading={false} showPopover={showPopover} - testid={`remove-groups-button-${id}`} + testid={`remove-groups-button-${name}`} handleDeleteItem={handleDeleteGroup} handleButtonClick={handleRemoveIconClick} /> diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesView/MessagesView.spec.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesView/MessagesView.spec.tsx index 61349714c6..7a65c3f0b0 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesView/MessagesView.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesView/MessagesView.spec.tsx @@ -1,12 +1,24 @@ import React from 'react' import { instance, mock } from 'ts-mockito' import { render } from 'uiSrc/utils/test-utils' -import GroupsView, { Props } from './MessagesView' +import { PendingEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' +import MessagesView, { Props } from './MessagesView' const mockedProps = mock() +const mockMessages: PendingEntryDto[] = [{ + id: '123', + consumerName: 'test', + idle: 321, + delivered: 321, +}, { + id: '1234', + consumerName: 'test2', + idle: 3213, + delivered: 1321, +}] -describe('GroupsView', () => { +describe('MessagesView', () => { it('should render', () => { - expect(render()).toBeTruthy() + expect(render()).toBeTruthy() }) }) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesView/MessagesView.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesView/MessagesView.tsx index eb267cea40..a9f9a63ead 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesView/MessagesView.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesView/MessagesView.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import { useSelector } from 'react-redux' import cx from 'classnames' import { orderBy } from 'lodash' @@ -34,13 +34,17 @@ const MessagesView = (props: Props) => { const [messages, setMessages] = useState(data) const [sortedColumnName, setSortedColumnName] = useState('id') - const [sortedColumnOrder, setSortedColumnOrder] = useState(SortOrder.DESC) + const [sortedColumnOrder, setSortedColumnOrder] = useState(SortOrder.ASC) + + useEffect(() => { + setMessages(orderBy(data, sortedColumnName, sortedColumnOrder?.toLowerCase())) + }, [data]) const onChangeSorting = (column: any, order: SortOrder) => { setSortedColumnName(column) setSortedColumnOrder(order) - setMessages(orderBy(messages, 'name', order?.toLowerCase())) + setMessages(orderBy(messages, column, order?.toLowerCase())) } return ( @@ -52,23 +56,23 @@ const MessagesView = (props: Props) => { styles.container, { footerOpened: isFooterOpen } )} - data-test-id="stream-messages-container" + data-testid="stream-messages-container" > div:hover { - background: var(--euiColorLightestShade); - - .value-table-actions { - background-color: var(--euiColorLightestShade) !important; - } - - .streamEntry { - color: var(--inputTextColor) !important; - } - - .streamEntryId { - color: var(--euiTextSubduedColor) !important; - } - } - } - - .ReactVirtualized__Table__headerRow { - border: none !important; - } - - .ReactVirtualized__Table__Grid { - border: 1px solid var(--tableDarkestBorderColor) !important; - } - } - - .cellHeader { - border: none !important; - } -} - -:global(.streamEntry) { - color: var(--euiTextSubduedColor) !important; - white-space: normal; - max-width: 100%; - word-break: break-all; -} - -:global(.streamEntryId) { - color: var(--euiColorMediumShade) !important; - display: flex; -} - -:global(.stream-entry-actions) { - margin-left: -5px; } .actions, .actionsHeader { width: 54px; } - -.actions { - :global(.value-table-actions) { - background-color: var(--euiColorEmptyShade) !important; - } -} diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesViewWrapper.spec.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesViewWrapper.spec.tsx index d1f098b65f..83c9f75f89 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesViewWrapper.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesViewWrapper.spec.tsx @@ -1,12 +1,73 @@ import React from 'react' import { instance, mock } from 'ts-mockito' -import { render } from 'uiSrc/utils/test-utils' +import { cloneDeep } from 'lodash' +import { cleanup, fireEvent, mockedStore, render, screen } from 'uiSrc/utils/test-utils' +import { loadConsumerGroups, setSelectedGroup } from 'uiSrc/slices/browser/stream' +import VirtualTable from 'uiSrc/components/virtual-table/VirtualTable' +import { PendingEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' +import MessagesView, { Props as MessagesViewProps } from './MessagesView' import MessagesViewWrapper, { Props } from './MessagesViewWrapper' const mockedProps = mock() +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +jest.mock('./MessagesView', () => ({ + __esModule: true, + namedExport: jest.fn(), + default: jest.fn(), +})) + +const mockMessages: PendingEntryDto[] = [{ + id: '123', + consumerName: 'test', + idle: 321, + delivered: 321, +}, { + id: '1234', + consumerName: 'test2', + idle: 3213, + delivered: 1321, +}] + +const mockMessagesView = (props: MessagesViewProps) => ( +
+ + +
+) + describe('MessagesViewWrapper', () => { + beforeAll(() => { + MessagesView.mockImplementation(mockMessagesView) + }) + it('should render', () => { expect(render()).toBeTruthy() }) + + it('should render Messages container', () => { + render() + + expect(screen.getByTestId('stream-messages-container')).toBeInTheDocument() + }) + + it.skip('should claim Message', () => { + render() + + const afterRenderActions = [...store.getActions()] + + fireEvent.click(screen.getByTestId('claim-message-btn')) + + expect(store.getActions()).toEqual([...afterRenderActions, setSelectedGroup(), loadConsumerGroups(false)]) + }) }) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesViewWrapper.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesViewWrapper.tsx index bd482a95e8..ce089a0b63 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesViewWrapper.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessagesViewWrapper.tsx @@ -1,11 +1,12 @@ import { EuiText } from '@elastic/eui' -import React, { useCallback, useState } from 'react' -import { useSelector } from 'react-redux' +import React, { useCallback, useEffect, useState } from 'react' +import { useSelector, useDispatch } from 'react-redux' import { selectedConsumerSelector } from 'uiSrc/slices/browser/stream' import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces' import { getFormatTime } from 'uiSrc/utils/streamUtils' import { TableCellTextAlignment } from 'uiSrc/constants' +import { updateSelectedKeyRefreshTime } from 'uiSrc/slices/browser/keys' import { PendingEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' import MessagesView from './MessagesView' @@ -16,18 +17,24 @@ const actionsWidth = 50 const minColumnWidth = 190 const suffix = '_stream_messages' -interface Props { +export interface Props { isFooterOpen: boolean } const MessagesViewWrapper = (props: Props) => { const { - data: loadedMessages = [], + lastRefreshTime, + data: loadedMessages = [] } = useSelector(selectedConsumerSelector) ?? {} - const [messages, setMessages] = useState(loadedMessages) const [claiming, setClaiming] = useState('') + const dispatch = useDispatch() + + useEffect(() => { + dispatch(updateSelectedKeyRefreshTime(lastRefreshTime)) + }, []) + const closePopover = useCallback(() => { setClaiming('') }, []) @@ -44,18 +51,18 @@ const MessagesViewWrapper = (props: Props) => { minWidth: minColumnWidth, isSortable: true, className: styles.cell, - headerClassName: styles.cellHeader, + headerClassName: 'streamItemHeader', render: function Id(_name: string, { id }: PendingEntryDto) { const timestamp = id.split('-')?.[0] return (
-
+
{getFormatTime(timestamp)}
-
+
{id}
@@ -70,8 +77,7 @@ const MessagesViewWrapper = (props: Props) => { absoluteWidth: 106, truncateText: true, isSortable: true, - // render: (cellData: ConnectionType) => - // capitalize(cellData), + headerClassName: 'streamItemHeader', }, { id: 'delivered', @@ -80,13 +86,12 @@ const MessagesViewWrapper = (props: Props) => { absoluteWidth: 106, truncateText: true, isSortable: true, - // render: (cellData: ConnectionType) => - // capitalize(cellData), + headerClassName: 'streamItemHeader', }, { id: 'actions', label: '', - headerClassName: styles.actionsHeader, + headerClassName: 'streamItemHeader', textAlignment: TableCellTextAlignment.Left, absoluteWidth: actionsWidth, maxWidth: actionsWidth, @@ -102,7 +107,7 @@ const MessagesViewWrapper = (props: Props) => { return ( <> { styles.container, { footerOpened: isFooterOpen } )} - data-test-id="stream-entries-container" + data-testid="stream-entries-container" > {/*
diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView/styles.module.scss b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView/styles.module.scss index 0207f67bc1..be0727e2e2 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView/styles.module.scss @@ -4,70 +4,6 @@ width: 100%; padding-top: 3px; background-color: var(--euiColorEmptyShade); - - :global { - .ReactVirtualized__Grid__innerScrollContainer { - .ReactVirtualized__Table__rowColumn { - border-right: 1px solid var(--tableDarkestBorderColor) !important; - - &:last-of-type, - &:nth-last-of-type(2) { - border-right: none !important; - } - } - - .ReactVirtualized__Table__row { - border-bottom: 1px solid var(--tableDarkestBorderColor) !important; - &:last-of-type { - border-bottom: none !important; - } - } - - & > div:hover { - background: var(--euiColorLightestShade); - - .value-table-actions { - background-color: var(--euiColorLightestShade) !important; - } - - .streamEntry { - color: var(--inputTextColor) !important; - } - - .streamEntryId { - color: var(--euiTextSubduedColor) !important; - } - } - } - - .ReactVirtualized__Table__headerRow { - border: none !important; - } - - .ReactVirtualized__Table__Grid { - border: 1px solid var(--tableDarkestBorderColor) !important; - } - } - - .cellHeader { - border: none !important; - } -} - -:global(.streamEntry) { - color: var(--euiTextSubduedColor) !important; - white-space: normal; - max-width: 100%; - word-break: break-all; -} - -:global(.streamEntryId) { - color: var(--euiColorMediumShade) !important; - display: flex; -} - -:global(.stream-entry-actions) { - margin-left: -5px; } .actions, @@ -75,12 +11,6 @@ width: 54px; } -.actions { - :global(.value-table-actions) { - background-color: var(--euiColorEmptyShade) !important; - } -} - .columnManager { z-index: 11; position: absolute; diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.spec.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.spec.tsx index 5c8a4cfa0c..8f44ddf34b 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.spec.tsx @@ -1,6 +1,6 @@ import React from 'react' import { instance, mock } from 'ts-mockito' -import { render } from 'uiSrc/utils/test-utils' +import { render, screen } from 'uiSrc/utils/test-utils' import StreamDataViewWrapper, { Props } from './StreamDataViewWrapper' const mockedProps = mock() @@ -9,4 +9,10 @@ describe('StreamDataViewWrapper', () => { it('should render', () => { expect(render()).toBeTruthy() }) + + it('should render Stream Data container', () => { + render() + + expect(screen.getByTestId('stream-entries-container')).toBeInTheDocument() + }) }) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx index 0787fab750..2764dd3754 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx @@ -1,4 +1,4 @@ -import { EuiText, EuiToolTip } from '@elastic/eui' +import { EuiText, EuiToolTip } from '@elastic/eui' import React, { useCallback, useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { keyBy } from 'lodash' @@ -11,7 +11,7 @@ import { getFormatTime } from 'uiSrc/utils/streamUtils' import { KeyTypes, TableCellTextAlignment } from 'uiSrc/constants' import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' -import { keysSelector } from 'uiSrc/slices/browser/keys' +import { keysSelector, updateSelectedKeyRefreshTime } from 'uiSrc/slices/browser/keys' import { StreamEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' import StreamDataView from './StreamDataView' @@ -33,18 +33,24 @@ export interface Props { const StreamDataViewWrapper = (props: Props) => { const { entries: loadedEntries = [], - keyName: key + keyName: key, + lastRefreshTime } = useSelector(streamDataSelector) const { id: instanceId } = useSelector(connectedInstanceSelector) const { viewType: browserViewType } = useSelector(keysSelector) const dispatch = useDispatch() - const [uniqFields, setUniqFields] = useState({}) + // for Manager columns + // const [uniqFields, setUniqFields] = useState({}) const [entries, setEntries] = useState([]) const [columns, setColumns] = useState([]) const [deleting, setDeleting] = useState('') + useEffect(() => { + dispatch(updateSelectedKeyRefreshTime(lastRefreshTime)) + }, []) + useEffect(() => { let fields = {} const streamEntries: IStreamEntry[] = loadedEntries?.map((item) => { @@ -59,7 +65,8 @@ const StreamDataViewWrapper = (props: Props) => { } }) - setUniqFields(fields) + // for Manager columns + // setUniqFields(fields) setEntries(streamEntries) setColumns([idColumn, ...Object.keys(fields).map((field) => getTemplateColumn(field)), actionsColumn]) }, [loadedEntries, deleting]) @@ -122,7 +129,7 @@ const StreamDataViewWrapper = (props: Props) => { minWidth: minColumnWidth, isSortable: false, className: styles.cell, - headerClassName: styles.cellHeader, + headerClassName: 'streamItemHeader', headerCellClassName: 'truncateText', render: function Id(_name: string, { id, fields }: StreamEntryDto) { const value = fields[label] ?? '' @@ -133,13 +140,13 @@ const StreamDataViewWrapper = (props: Props) => {
@@ -159,18 +166,18 @@ const StreamDataViewWrapper = (props: Props) => { minWidth: minColumnWidth, isSortable: true, className: styles.cell, - headerClassName: styles.cellHeader, + headerClassName: 'streamItemHeader', render: function Id(_name: string, { id }: StreamEntryDto) { const timestamp = id.split('-')?.[0] return (
-
+
{getFormatTime(timestamp)}
-
+
{id}
diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.spec.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.spec.tsx new file mode 100644 index 0000000000..c55fb85fd8 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.spec.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import { render } from 'uiSrc/utils/test-utils' +import StreamTabs from './StreamTabs' + +describe('StreamTabs', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.tsx index 3683ccc682..14ad3ce2be 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useCallback } from 'react' import { EuiTab, EuiTabs } from '@elastic/eui' import { useDispatch, useSelector } from 'react-redux' @@ -7,55 +7,60 @@ import { setStreamViewType, fetchConsumerGroups, streamGroupsSelector, + selectedGroupSelector, + selectedConsumerSelector, } from 'uiSrc/slices/browser/stream' import { StreamViewType } from 'uiSrc/slices/interfaces/stream' import { streamViewTypeTabs } from '../constants' const StreamTabs = () => { - const { viewType, data: { entries = [] } } = useSelector(streamSelector) + const { viewType } = useSelector(streamSelector) const { data: groups = [] } = useSelector(streamGroupsSelector) + const { name: selectedGroupName = '' } = useSelector(selectedGroupSelector) ?? {} + const { name: selectedConsumerName = '' } = useSelector(selectedConsumerSelector) ?? {} const dispatch = useDispatch() const onSelectedTabChanged = (id: StreamViewType) => { - dispatch(setStreamViewType(id)) - - if (id === StreamViewType.Data && entries?.length === 0) { - // dispatch(fetchConsumerGroups()) - } if (id === StreamViewType.Groups && groups.length === 0) { dispatch(fetchConsumerGroups()) } + dispatch(setStreamViewType(id)) } - const getSelectedTab = (id:StreamViewType) => { - if (id === StreamViewType.Data && viewType === id) { - return true - } + const renderTabs = useCallback(() => { + const tabs = [...streamViewTypeTabs] - if (id === StreamViewType.Groups - && (viewType === id || viewType === StreamViewType.Consumers || viewType === StreamViewType.Messages)) { - return true + if (selectedGroupName && (viewType === StreamViewType.Consumers || viewType === StreamViewType.Messages)) { + tabs.push({ + id: StreamViewType.Consumers, + label: selectedGroupName, + }) } - return false - } + if (selectedConsumerName && viewType === StreamViewType.Messages) { + tabs.push({ + id: StreamViewType.Messages, + label: selectedConsumerName + }) + } - const renderTabs = () => - streamViewTypeTabs.map(({ id, label }, i) => ( + return tabs.map(({ id, label }) => ( onSelectedTabChanged(id)} - // eslint-disable-next-line react/no-array-index-key - key={i} + key={id} > {label} )) + }, [viewType, selectedGroupName, selectedConsumerName]) return ( - {renderTabs()} + <> + {renderTabs()} + ) } diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/styles.module.scss b/redisinsight/ui/src/pages/browser/components/stream-details/styles.module.scss index dc283b80b8..501d63a71c 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/stream-details/styles.module.scss @@ -6,6 +6,60 @@ :global(.stream-details-table) { height: calc(100% - 125px); } + + :global { + .ReactVirtualized__Grid__innerScrollContainer { + .ReactVirtualized__Table__rowColumn { + padding-right: 6px !important; + border-right: 1px solid var(--tableDarkestBorderColor) !important; + + > div { + min-height: 54px !important; + } + + &:last-of-type, + &:nth-last-of-type(2) { + border-right: none !important; + } + } + + .ReactVirtualized__Table__row { + border-bottom: 1px solid var(--tableDarkestBorderColor) !important; + &:last-of-type { + border-bottom: none !important; + } + } + + & > div:hover { + background: var(--euiColorLightestShade); + + .value-table-actions { + background-color: var(--euiColorLightestShade) !important; + } + + .streamItem { + color: var(--inputTextColor) !important; + } + + .streamItemId { + color: var(--euiTextSubduedColor) !important; + } + } + } + + .ReactVirtualized__Table__headerRow { + border: none !important; + + .streamItemHeader { + padding-right: 6px !important; + border: none !important; + } + } + + .ReactVirtualized__Table__Grid { + border: 1px solid var(--tableDarkestBorderColor) !important; + } + } } .rangeWrapper { @@ -26,3 +80,19 @@ margin-top: 2px; z-index: 1; } + +:global(.streamItem) { + color: var(--euiTextSubduedColor) !important; + white-space: normal; + max-width: 100%; + word-break: break-all; +} + +:global(.streamItemId) { + color: var(--euiColorMediumShade) !important; + display: flex; +} + +:global(.stream-entry-actions) { + margin-left: -5px; +} diff --git a/redisinsight/ui/src/slices/browser/keys.ts b/redisinsight/ui/src/slices/browser/keys.ts index 87d7395070..a09dbd8f64 100644 --- a/redisinsight/ui/src/slices/browser/keys.ts +++ b/redisinsight/ui/src/slices/browser/keys.ts @@ -30,7 +30,7 @@ import { fetchSetMembers } from './set' import { fetchReJSON } from './rejson' import { setHashInitialState, fetchHashFields } from './hash' import { setListInitialState, fetchListElements } from './list' -import { fetchConsumerGroups, fetchStreamEntries } from './stream' +import { fetchStreamEntries, setStreamInitialState } from './stream' import { addErrorNotification, addMessageNotification } from '../app/notifications' import { KeysStore, KeyViewType } from '../interfaces/keys' import { AppDispatch, RootState } from '../store' @@ -366,6 +366,9 @@ export function setInitialStateByType(type: string) { if (type === KeyTypes.ZSet) { dispatch(setZsetInitialState()) } + if (type === KeyTypes.Stream) { + dispatch(setStreamInitialState()) + } } } // Asynchronous thunk action diff --git a/redisinsight/ui/src/slices/browser/stream.ts b/redisinsight/ui/src/slices/browser/stream.ts index 2dde0dace7..bbc09cd721 100644 --- a/redisinsight/ui/src/slices/browser/stream.ts +++ b/redisinsight/ui/src/slices/browser/stream.ts @@ -26,7 +26,6 @@ export const initialState: StateStream = { sortOrder: SortOrder.DESC, range: { start: '', end: '' }, viewType: StreamViewType.Data, - // viewType: StreamViewType.Groups, data: { total: 0, entries: [], @@ -40,6 +39,7 @@ export const initialState: StateStream = { id: '', fields: {} }, + lastRefreshTime: null, }, groups: { loading: false, @@ -55,6 +55,7 @@ const streamSlice = createSlice({ name: 'stream', initialState, reducers: { + setStreamInitialState: () => initialState, // load stream entries loadEntries: (state, { payload: resetData = true }: PayloadAction>) => { state.loading = true @@ -71,6 +72,7 @@ const streamSlice = createSlice({ ...data, } state.data.keyName = data?.keyName + state.data.lastRefreshTime = Date.now() state.sortOrder = sortOrder state.loading = false }, @@ -147,7 +149,7 @@ const streamSlice = createSlice({ loadConsumerGroupsSuccess: (state, { payload }: PayloadAction) => { state.groups.loading = false state.groups.data = payload - state.groups.data.lastRefreshTime = Date.now() + state.groups.lastRefreshTime = Date.now() }, loadConsumerGroupsFailure: (state, { payload }) => { state.groups.loading = false @@ -174,19 +176,31 @@ const streamSlice = createSlice({ } }, + loadConsumersFailure: (state, { payload }: PayloadAction) => { + state.groups.loading = false + state.groups.error = payload + state.viewType = StreamViewType.Groups + }, + loadConsumerMessagesSuccess: (state, { payload }: PayloadAction) => { state.groups.loading = false state.groups.selectedGroup = { ...state.groups.selectedGroup, selectedConsumer: { - ...state.groups.selectedGroup.selectedConsumer, + ...state.groups.selectedGroup?.selectedConsumer, lastRefreshTime: Date.now(), data: payload, } } }, + loadConsumerMessagesFailure: (state, { payload }: PayloadAction) => { + state.groups.loading = false + state.groups.error = payload + state.viewType = StreamViewType.Consumers + }, + setConsumerGroupsSortOrder: (state, { payload }: PayloadAction) => { state.groups.sortOrder = payload }, @@ -199,6 +213,7 @@ const streamSlice = createSlice({ // Actions generated from the slice export const { + setStreamInitialState, loadEntries, loadEntriesSuccess, loadEntriesFailure, @@ -219,7 +234,9 @@ export const { loadConsumerGroupsSuccess, loadConsumerGroupsFailure, loadConsumersSuccess, + loadConsumersFailure, loadConsumerMessagesSuccess, + loadConsumerMessagesFailure, setSelectedGroup, setSelectedConsumer, } = streamSlice.actions @@ -478,7 +495,7 @@ export function fetchConsumerGroups( ApiEndpoints.STREAMS_CONSUMER_GROUPS_GET ), { - keyName: state.browser.keys.selectedKey.data?.name, + keyName: state.browser.keys?.selectedKey?.data?.name, }, ) @@ -515,7 +532,7 @@ export function fetchConsumers( ApiEndpoints.STREAMS_CONSUMERS_GET ), { - keyName: state.browser.keys.selectedKey.data?.name, + keyName: state.browser.keys?.selectedKey?.data?.name, groupName: state.browser.stream.groups.selectedGroup?.name, }, ) @@ -529,7 +546,7 @@ export function fetchConsumers( const error = _err as AxiosError const errorMessage = getApiErrorMessage(error) dispatch(addErrorNotification(error)) - dispatch(loadConsumerGroupsFailure(errorMessage)) + dispatch(loadConsumersFailure(errorMessage)) onFailed?.() } } @@ -553,7 +570,7 @@ export function fetchConsumerMessages( ApiEndpoints.STREAMS_CONSUMERS_MESSAGES_GET ), { - keyName: state.browser.keys.selectedKey.data?.name, + keyName: state.browser.keys?.selectedKey?.data?.name, groupName: state.browser.stream.groups.selectedGroup?.name, consumerName: state.browser.stream.groups.selectedGroup?.selectedConsumer?.name }, @@ -567,7 +584,7 @@ export function fetchConsumerMessages( const error = _err as AxiosError const errorMessage = getApiErrorMessage(error) dispatch(addErrorNotification(error)) - dispatch(loadConsumerGroupsFailure(errorMessage)) + dispatch(loadConsumerMessagesFailure(errorMessage)) onFailed?.() } } diff --git a/redisinsight/ui/src/slices/interfaces/stream.ts b/redisinsight/ui/src/slices/interfaces/stream.ts index c254ae73e2..d76736aa3f 100644 --- a/redisinsight/ui/src/slices/interfaces/stream.ts +++ b/redisinsight/ui/src/slices/interfaces/stream.ts @@ -24,11 +24,15 @@ export interface StateStream { error: string sortOrder: SortOrder range: Range - data: GetStreamEntriesResponse + data: StateStreamData viewType: StreamViewType groups: StateConsumerGroups } +export interface StateStreamData extends GetStreamEntriesResponse { + lastRefreshTime: Nullable +} + export interface StateConsumerGroups { loading: boolean error: string diff --git a/redisinsight/ui/src/slices/tests/browser/stream.spec.ts b/redisinsight/ui/src/slices/tests/browser/stream.spec.ts index 0b947a96aa..490731b4c6 100644 --- a/redisinsight/ui/src/slices/tests/browser/stream.spec.ts +++ b/redisinsight/ui/src/slices/tests/browser/stream.spec.ts @@ -1,30 +1,53 @@ +import { ConsumerDto, ConsumerGroupDto, PendingEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' import { AxiosError } from 'axios' -import { cloneDeep } from 'lodash' -import { DEFAULT_SLOWLOG_DURATION_UNIT, SortOrder } from 'uiSrc/constants' +import { cloneDeep, omit } from 'lodash' +import { SortOrder } from 'uiSrc/constants' import { apiService } from 'uiSrc/services' import reducer, { - addNewEntries, - addNewEntriesFailure, - addNewEntriesSuccess, - cleanRangeFilter, - fetchStreamEntries, initialState, + setStreamInitialState, loadEntries, - loadEntriesFailure, loadEntriesSuccess, + loadEntriesFailure, loadMoreEntries, + loadMoreEntriesSuccess, loadMoreEntriesFailure, - loadMoreEntriesSuccess, refreshStreamEntries, - removeEntriesFromList, + addNewEntries, + addNewEntriesSuccess, + addNewEntriesFailure, removeStreamEntries, - removeStreamEntriesFailure, removeStreamEntriesSuccess, - streamRangeSelector, - streamSelector, + removeStreamEntriesFailure, + updateStart, updateEnd, - updateStart + cleanRangeFilter, + streamSelector, + streamRangeSelector, + fetchStreamEntries, + refreshStreamEntries, + setStreamViewType, + loadConsumerGroups, + loadConsumerGroupsSuccess, + loadConsumerGroupsFailure, + loadConsumersSuccess, + loadConsumersFailure, + loadConsumerMessagesSuccess, + loadConsumerMessagesFailure, + setSelectedGroup, + setSelectedConsumer, + streamDataSelector, + streamGroupsSelector, + streamGroupsDataSelector, + selectedGroupSelector, + selectedConsumerSelector, + fetchMoreStreamEntries, + addNewEntriesAction, + deleteStreamEntry, + fetchConsumerGroups, + fetchConsumers, + fetchConsumerMessages, } from 'uiSrc/slices/browser/stream' -import { fetchSlowLogsAction, getSlowLogs, getSlowLogsError, getSlowLogsSuccess } from 'uiSrc/slices/slowlog/slowlog' +import { StreamViewType } from 'uiSrc/slices/interfaces/stream' import { cleanup, initialStateDefault, mockedStore, } from 'uiSrc/utils/test-utils' import { addErrorNotification } from '../../app/notifications' @@ -32,7 +55,7 @@ jest.mock('uiSrc/services') let store: typeof mockedStore -const mockedData = { +const mockedEntryData = { keyName: 'stream_example', total: 1, lastGeneratedId: '1652942518810-0', @@ -50,6 +73,44 @@ const mockedData = { }] } +const mockGroups: ConsumerGroupDto[] = [{ + name: 'test', + consumers: 123, + pending: 321, + smallestPendingId: '123', + greatestPendingId: '123', + lastDeliveredId: '123' +}, { + name: 'test2', + consumers: 13, + pending: 31, + smallestPendingId: '3', + greatestPendingId: '23', + lastDeliveredId: '12' +}] + +const mockConsumers: ConsumerDto[] = [{ + name: 'test', + idle: 123, + pending: 321, +}, { + name: 'test2', + idle: 13, + pending: 31, +}] + +const mockMessages: PendingEntryDto[] = [{ + id: '123', + consumerName: 'test', + idle: 321, + delivered: 321, +}, { + id: '1234', + consumerName: 'test2', + idle: 3213, + delivered: 1321, +}] + beforeEach(() => { cleanup() store = cloneDeep(mockedStore) @@ -70,6 +131,16 @@ describe('stream slice', () => { }) }) + describe('setStreamInitialState', () => { + it('should properly set initial state', () => { + const nextState = reducer(initialState, setStreamInitialState()) + const rootState = Object.assign(initialStateDefault, { + browser: { stream: nextState }, + }) + expect(streamSelector(rootState)).toEqual(initialState) + }) + }) + describe('loadEntries', () => { it('should properly set the state before the fetch data', () => { // Arrange @@ -98,12 +169,14 @@ describe('stream slice', () => { loading: false, error: '', data: { - ...mockedData, + ...mockedEntryData, }, } // Act - const nextState = reducer(initialState, loadEntriesSuccess([mockedData, SortOrder.DESC])) + const tempState = reducer(initialState, loadEntriesSuccess([mockedEntryData, SortOrder.DESC])) + + const nextState = omit({ ...tempState }, 'data.lastRefreshTime') // Assert const rootState = Object.assign(initialStateDefault, { @@ -143,7 +216,7 @@ describe('stream slice', () => { } // Act - const nextState = reducer(initialState, loadMoreEntries(true)) + const nextState = reducer(initialState, loadMoreEntries()) // Assert const rootState = Object.assign(initialStateDefault, { @@ -162,12 +235,13 @@ describe('stream slice', () => { loading: false, error: '', data: { - ...mockedData, + ...mockedEntryData, }, } // Act - const nextState = reducer(initialState, loadMoreEntriesSuccess(mockedData)) + const tempState = reducer(initialState, loadMoreEntriesSuccess(mockedEntryData)) + const nextState = omit({ ...tempState }, 'data.lastRefreshTime') // Assert const rootState = Object.assign(initialStateDefault, { @@ -388,17 +462,277 @@ describe('stream slice', () => { }) }) + describe('setStreamViewType', () => { + it('should properly set stream view type', () => { + // Arrange + const state = { + ...initialState, + viewType: StreamViewType.Messages, + } + + // Act + const nextState = reducer(initialState, setStreamViewType(StreamViewType.Messages)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + browser: { stream: nextState }, + }) + expect(streamSelector(rootState)).toEqual(state) + }) + }) + + describe('loadConsumerGroups', () => { + it('should properly set groups.loading = true', () => { + // Arrange + const state = { + ...initialState, + loading: false, + groups: { + ...initialState.groups, + loading: true, + } + } + + // Act + const nextState = reducer(initialState, loadConsumerGroups()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + browser: { stream: nextState }, + }) + expect(streamSelector(rootState)).toEqual(state) + }) + }) + + describe('loadConsumerGroupsSuccess', () => { + it('should properly set groups.data = payload', () => { + // Arrange + const data: ConsumerGroupDto[] = [{ + name: '123', + consumers: 123, + pending: 123, + smallestPendingId: '123', + greatestPendingId: '123', + lastDeliveredId: '123', + }] + const state = { + ...initialState, + loading: false, + groups: { + ...initialState.groups, + data, + lastRefreshTime: Date.now(), + loading: false, + } + } + + // Act + const nextState = reducer(initialState, loadConsumerGroupsSuccess(data)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + browser: { stream: nextState }, + }) + expect(streamSelector(rootState)).toEqual(state) + }) + }) + + describe('loadConsumersSuccess', () => { + it('should properly set groups.selectedGroup.data = payload', () => { + // Arrange + const data: ConsumerDto[] = [{ + name: '123', + pending: 123, + idle: 123, + }] + const state = { + ...initialState, + groups: { + ...initialState.groups, + selectedGroup: { + data, + lastRefreshTime: Date.now(), + } + } + } + + // Act + const nextState = reducer(initialState, loadConsumersSuccess(data)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + browser: { stream: nextState }, + }) + expect(streamSelector(rootState)).toEqual(state) + }) + }) + + describe('loadConsumerMessagesSuccess', () => { + it('should properly set groups.selectedGroup.selectedConsumer.data = payload', () => { + // Arrange + const data: PendingEntryDto[] = [{ + id: '123', + consumerName: '123', + idle: 123, + delivered: 123, + }] + const state = { + ...initialState, + groups: { + ...initialState.groups, + selectedGroup: { + selectedConsumer: { + data, + lastRefreshTime: Date.now(), + } + } + } + } + + // Act + const nextState = reducer(initialState, loadConsumerMessagesSuccess(data)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + browser: { stream: nextState }, + }) + expect(streamSelector(rootState)).toEqual(state) + }) + }) + + describe('loadConsumerMessagesFailure', () => { + it('should properly set error to groups and set viewType = Consumers payload', () => { + // Arrange + const error = 'Some error' + const state = { + ...initialState, + viewType: StreamViewType.Consumers, + groups: { + ...initialState.groups, + loading: false, + error, + } + } + + // Act + const nextState = reducer(initialState, loadConsumerMessagesFailure(error)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + browser: { stream: nextState }, + }) + expect(streamSelector(rootState)).toEqual(state) + }) + }) + + describe('loadConsumersFailure', () => { + it('should properly set error to groups and set viewType = Groups payload', () => { + // Arrange + const error = 'Some error' + const state = { + ...initialState, + viewType: StreamViewType.Groups, + groups: { + ...initialState.groups, + loading: false, + error, + } + } + + // Act + const nextState = reducer(initialState, loadConsumersFailure(error)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + browser: { stream: nextState }, + }) + expect(streamSelector(rootState)).toEqual(state) + }) + }) + + describe('loadConsumerGroupsFailure', () => { + it('should properly set error to groups payload', () => { + // Arrange + const error = 'Some error' + const state = { + ...initialState, + groups: { + ...initialState.groups, + loading: false, + error, + } + } + + // Act + const nextState = reducer(initialState, loadConsumerGroupsFailure(error)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + browser: { stream: nextState }, + }) + expect(streamSelector(rootState)).toEqual(state) + }) + }) + + describe('setSelectedGroup', () => { + it('should properly set selectedGroups', () => { + // Arrange + const group = { name: 'group name' } + const state = { + ...initialState, + groups: { + ...initialState.groups, + selectedGroup: group, + } + } + + // Act + const nextState = reducer(initialState, setSelectedGroup(group)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + browser: { stream: nextState }, + }) + expect(streamSelector(rootState)).toEqual(state) + }) + }) + + describe('setSelectedConsumer', () => { + it('should properly set selectedConsumer', () => { + // Arrange + const consumer = { name: 'consumer name' } + const state = { + ...initialState, + groups: { + ...initialState.groups, + selectedGroup: { + selectedConsumer: consumer + } + } + } + + // Act + const nextState = reducer(initialState, setSelectedConsumer(consumer)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + browser: { stream: nextState }, + }) + expect(streamSelector(rootState)).toEqual(state) + }) + }) + describe('thunks', () => { describe('fetchStreamEntries', () => { it('succeed to fetch data', async () => { // Arrange - const responsePayload = { data: mockedData, status: 200 } + const responsePayload = { data: mockedEntryData, status: 200 } apiService.post = jest.fn().mockResolvedValue(responsePayload) // Act await store.dispatch(fetchStreamEntries( - mockedData.keyName, + mockedEntryData.keyName, 500, SortOrder.DESC, true @@ -407,7 +741,7 @@ describe('stream slice', () => { // Assert const expectedActions = [ loadEntries(true), - loadEntriesSuccess([mockedData, SortOrder.DESC]), + loadEntriesSuccess([mockedEntryData, SortOrder.DESC]), ] expect(store.getActions()).toEqual(expectedActions) @@ -426,7 +760,7 @@ describe('stream slice', () => { // Act await store.dispatch(fetchStreamEntries( - mockedData.keyName, + mockedEntryData.keyName, 500, SortOrder.DESC, true @@ -446,20 +780,20 @@ describe('stream slice', () => { describe('refreshStreamEntries', () => { it('succeed to fetch data', async () => { // Arrange - const responsePayload = { data: mockedData, status: 200 } + const responsePayload = { data: mockedEntryData, status: 200 } apiService.post = jest.fn().mockResolvedValue(responsePayload) // Act await store.dispatch(refreshStreamEntries( - mockedData.keyName, + mockedEntryData.keyName, true )) // Assert const expectedActions = [ loadEntries(true), - loadEntriesSuccess([mockedData, SortOrder.DESC]), + loadEntriesSuccess([mockedEntryData, SortOrder.DESC]), ] expect(store.getActions()).toEqual(expectedActions) @@ -478,7 +812,7 @@ describe('stream slice', () => { // Act await store.dispatch(refreshStreamEntries( - mockedData.keyName, + mockedEntryData.keyName, true )) @@ -492,5 +826,137 @@ describe('stream slice', () => { expect(store.getActions()).toEqual(expectedActions) }) }) + + describe('fetchConsumerGroups', () => { + it('succeed to fetch data', async () => { + // Arrange + const responsePayload = { data: mockGroups, status: 200 } + + apiService.post = jest.fn().mockResolvedValue(responsePayload) + + // Act + await store.dispatch(fetchConsumerGroups()) + + // Assert + const expectedActions = [ + loadConsumerGroups(), + loadConsumerGroupsSuccess(mockGroups), + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + + it('failed to fetch data', async () => { + const errorMessage = 'Something was wrong!' + const responsePayload = { + response: { + status: 500, + data: { message: errorMessage }, + }, + } + + apiService.post = jest.fn().mockRejectedValue(responsePayload) + + // Act + await store.dispatch(fetchConsumerGroups()) + + // Assert + const expectedActions = [ + loadConsumerGroups(), + addErrorNotification(responsePayload as AxiosError), + loadConsumerGroupsFailure(errorMessage) + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + }) + + describe('fetchConsumers', () => { + it('succeed to fetch data', async () => { + // Arrange + const responsePayload = { data: mockConsumers, status: 200 } + + apiService.post = jest.fn().mockResolvedValue(responsePayload) + + // Act + await store.dispatch(fetchConsumers()) + + // Assert + const expectedActions = [ + loadConsumerGroups(), + loadConsumersSuccess(mockConsumers), + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + + it('failed to fetch data', async () => { + const errorMessage = 'Something was wrong!' + const responsePayload = { + response: { + status: 500, + data: { message: errorMessage }, + }, + } + + apiService.post = jest.fn().mockRejectedValue(responsePayload) + + // Act + await store.dispatch(fetchConsumers()) + + // Assert + const expectedActions = [ + loadConsumerGroups(), + addErrorNotification(responsePayload as AxiosError), + loadConsumersFailure(errorMessage) + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + }) + + describe('fetchConsumerMessages', () => { + it('succeed to fetch data', async () => { + // Arrange + const responsePayload = { data: mockMessages, status: 200 } + + apiService.post = jest.fn().mockResolvedValue(responsePayload) + + // Act + await store.dispatch(fetchConsumerMessages()) + + // Assert + const expectedActions = [ + loadConsumerGroups(), + loadConsumerMessagesSuccess(mockMessages), + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + + it('failed to fetch data', async () => { + const errorMessage = 'Something was wrong!' + const responsePayload = { + response: { + status: 500, + data: { message: errorMessage }, + }, + } + + apiService.post = jest.fn().mockRejectedValue(responsePayload) + + // Act + await store.dispatch(fetchConsumerMessages()) + + // Assert + const expectedActions = [ + loadConsumerGroups(), + addErrorNotification(responsePayload as AxiosError), + loadConsumerMessagesFailure(errorMessage) + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + }) }) }) diff --git a/redisinsight/ui/src/slices/tests/workbench/wb-results.spec.ts b/redisinsight/ui/src/slices/tests/workbench/wb-results.spec.ts index 06c3bcb1c9..ec6d1987f9 100644 --- a/redisinsight/ui/src/slices/tests/workbench/wb-results.spec.ts +++ b/redisinsight/ui/src/slices/tests/workbench/wb-results.spec.ts @@ -359,7 +359,7 @@ describe('workbench results slice', () => { expect(clearStoreActions(store.getActions())).toEqual(clearStoreActions(expectedActions)) }) - it.only('call both sendWBCommandClusterAction and processWBCommandFailure when fetch is fail', async () => { + it('call both sendWBCommandClusterAction and processWBCommandFailure when fetch is fail', async () => { // Arrange const command = 'keys *' const errorMessage = 'Could not connect to aoeu:123, please check the connection details.' diff --git a/redisinsight/ui/src/utils/tests/monacoUtils.spec.ts b/redisinsight/ui/src/utils/tests/monacoUtils.spec.ts index 6849869e22..81b4538b1d 100644 --- a/redisinsight/ui/src/utils/tests/monacoUtils.spec.ts +++ b/redisinsight/ui/src/utils/tests/monacoUtils.spec.ts @@ -106,7 +106,7 @@ describe('splitMonacoValuePerLines', () => { ) }) -describe.only('findArgIndexByCursor', () => { +describe('findArgIndexByCursor', () => { const cases = [ [ ['get', 'foo', 'bar'], diff --git a/yarn.lock b/yarn.lock index 035d2a7b36..c9b00d0431 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12440,16 +12440,16 @@ react-input-autosize@^2.2.2: dependencies: prop-types "^15.5.8" -"react-is@^16.12.0 || ^17.0.0", react-is@^17.0.1, react-is@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" - integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== - react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.4: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-is@^17.0.1, react-is@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + react-is@~16.3.0: version "16.3.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.3.2.tgz#f4d3d0e2f5fbb6ac46450641eb2e25bf05d36b22" @@ -12565,14 +12565,6 @@ react-router@5.2.0: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-shallow-renderer@^16.13.1: - version "16.14.1" - resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz#bf0d02df8a519a558fd9b8215442efa5c840e124" - integrity sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg== - dependencies: - object-assign "^4.1.1" - react-is "^16.12.0 || ^17.0.0" - react-style-singleton@^2.1.0, react-style-singleton@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.1.1.tgz#ce7f90b67618be2b6b94902a30aaea152ce52e66" @@ -12582,16 +12574,6 @@ react-style-singleton@^2.1.0, react-style-singleton@^2.1.1: invariant "^2.2.4" tslib "^1.0.0" -react-test-renderer@^17.0.1: - version "17.0.1" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-17.0.1.tgz#3187e636c3063e6ae498aedf21ecf972721574c7" - integrity sha512-/dRae3mj6aObwkjCcxZPlxDFh73XZLgvwhhyON2haZGUEhiaY5EjfAdw+d/rQmlcFwdTpMXCSGVk374QbCTlrA== - dependencies: - object-assign "^4.1.1" - react-is "^17.0.1" - react-shallow-renderer "^16.13.1" - scheduler "^0.20.1" - react-virtualized-auto-sizer@^1.0.2, react-virtualized-auto-sizer@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.6.tgz#66c5b1c9278064c5ef1699ed40a29c11518f97ca" From 5eeb3821c0349ffb880773ba3f1cd3b71e7378d6 Mon Sep 17 00:00:00 2001 From: "Roman.Sergeenko" Date: Fri, 27 May 2022 10:12:24 +0400 Subject: [PATCH 14/20] #RI-2930 - add interfaces, fix loading state --- .../add-stream-group/AddStreamGroup.tsx | 4 +++- redisinsight/ui/src/slices/browser/stream.ts | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/AddStreamGroup.tsx b/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/AddStreamGroup.tsx index 381faa628d..d1e3efbc5f 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/AddStreamGroup.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/AddStreamGroup.tsx @@ -13,9 +13,11 @@ import { import cx from 'classnames' import React, { ChangeEvent, useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' + import { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys' import { addNewGroupAction } from 'uiSrc/slices/browser/stream' import { consumerGroupIdRegex, validateConsumerGroupId } from 'uiSrc/utils' +import { CreateConsumerGroupsDto } from 'apiSrc/modules/browser/dto/stream.dto' import styles from './styles.module.scss' @@ -50,7 +52,7 @@ const AddStreamGroup = (props: Props) => { const submitData = () => { if (isFormValid) { - const data: any = { + const data: CreateConsumerGroupsDto = { keyName, consumerGroups: [{ name: groupName, diff --git a/redisinsight/ui/src/slices/browser/stream.ts b/redisinsight/ui/src/slices/browser/stream.ts index 2d43366041..b441dfd3e6 100644 --- a/redisinsight/ui/src/slices/browser/stream.ts +++ b/redisinsight/ui/src/slices/browser/stream.ts @@ -13,6 +13,7 @@ import { AddStreamEntriesResponse, ConsumerDto, ConsumerGroupDto, + CreateConsumerGroupsDto, GetStreamEntriesResponse, PendingEntryDto, } from 'apiSrc/modules/browser/dto/stream.dto' @@ -113,7 +114,7 @@ const streamSlice = createSlice({ state.error = '' }, addNewGroupSuccess: (state) => { - state.loading = true + state.loading = false }, addNewGroupFailure: (state, { payload }) => { state.loading = false @@ -494,7 +495,7 @@ export function deleteStreamEntry(key: string, entries: string[], onSuccessActio // Asynchronous thunk action export function addNewGroupAction( - data: any, + data: CreateConsumerGroupsDto, onSuccess?: () => void, onFail?: () => void ) { @@ -503,7 +504,7 @@ export function addNewGroupAction( try { const state = stateInit() - const { status } = await apiService.post( + const { status } = await apiService.post( getUrl( state.connections.instances.connectedInstance?.id, ApiEndpoints.STREAMS_CONSUMER_GROUPS From 8b542bce426840e3b8cb4e88e7420d0050130b50 Mon Sep 17 00:00:00 2001 From: "Roman.Sergeenko" Date: Fri, 27 May 2022 10:29:09 +0400 Subject: [PATCH 15/20] #RI-2930 - add unit tests --- .../ui/src/utils/tests/validations.spec.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/redisinsight/ui/src/utils/tests/validations.spec.ts b/redisinsight/ui/src/utils/tests/validations.spec.ts index 9de7b6481f..348429bab5 100644 --- a/redisinsight/ui/src/utils/tests/validations.spec.ts +++ b/redisinsight/ui/src/utils/tests/validations.spec.ts @@ -15,7 +15,8 @@ import { MAX_REFRESH_RATE, errorValidateRefreshRateNumber, errorValidateNegativeInteger, -} from '../validations' + validateConsumerGroupId +} from 'uiSrc/utils' const text1 = '123 123 123' const text2 = 'lorem lorem12312 lorem' @@ -248,4 +249,17 @@ describe('Validations utils', () => { expect(result).toBe(expected) }) }) + + describe('validateConsumerGroupId', () => { + it.each([ + ['123', '123'], + ['123-1', '123-1'], + ['$', '$'], + ['11.zx-1', '11-1'], + ])('for input: %s (input), should be output: %s', + (input, expected) => { + const result = validateConsumerGroupId(input) + expect(result).toBe(expected) + }) + }) }) From eafe31b18b4528723d995006a083992066c53593 Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Fri, 27 May 2022 11:06:21 +0400 Subject: [PATCH 16/20] #RI-2932 - removed useless interfaces and mocked Data.now --- redisinsight/ui/src/slices/browser/stream.ts | 10 +++++----- .../ui/src/slices/tests/browser/stream.spec.ts | 5 +++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/redisinsight/ui/src/slices/browser/stream.ts b/redisinsight/ui/src/slices/browser/stream.ts index bbc09cd721..c2e0d2f721 100644 --- a/redisinsight/ui/src/slices/browser/stream.ts +++ b/redisinsight/ui/src/slices/browser/stream.ts @@ -481,7 +481,7 @@ export function deleteStreamEntry(key: string, entries: string[], onSuccessActio // Asynchronous thunk action export function fetchConsumerGroups( resetData?: boolean, - onSuccess?: (data: GetStreamEntriesResponse) => void, + onSuccess?: () => void, onFailed?: () => void, ) { return async (dispatch: AppDispatch, stateInit: () => RootState) => { @@ -501,7 +501,7 @@ export function fetchConsumerGroups( if (isStatusSuccessful(status)) { dispatch(loadConsumerGroupsSuccess(data)) - onSuccess?.(data) + onSuccess?.() } } catch (_err) { if (!axios.isCancel(_err)) { @@ -518,7 +518,7 @@ export function fetchConsumerGroups( // Asynchronous thunk action export function fetchConsumers( resetData?: boolean, - onSuccess?: (data: GetStreamEntriesResponse) => void, + onSuccess?: () => void, onFailed?: () => void, ) { return async (dispatch: AppDispatch, stateInit: () => RootState) => { @@ -556,7 +556,7 @@ export function fetchConsumers( // Asynchronous thunk action export function fetchConsumerMessages( resetData?: boolean, - onSuccess?: (data: PendingEntryDto[]) => void, + onSuccess?: () => void, onFailed?: () => void, ) { return async (dispatch: AppDispatch, stateInit: () => RootState) => { @@ -577,7 +577,7 @@ export function fetchConsumerMessages( ) if (isStatusSuccessful(status)) { dispatch(loadConsumerMessagesSuccess(data)) - onSuccess?.(data) + onSuccess?.() } } catch (_err) { if (!axios.isCancel(_err)) { diff --git a/redisinsight/ui/src/slices/tests/browser/stream.spec.ts b/redisinsight/ui/src/slices/tests/browser/stream.spec.ts index 490731b4c6..dae57229a1 100644 --- a/redisinsight/ui/src/slices/tests/browser/stream.spec.ts +++ b/redisinsight/ui/src/slices/tests/browser/stream.spec.ts @@ -111,6 +111,8 @@ const mockMessages: PendingEntryDto[] = [{ delivered: 1321, }] +Date.now = jest.fn(() => Date.parse('2021-05-27')) + beforeEach(() => { cleanup() store = cloneDeep(mockedStore) @@ -507,6 +509,9 @@ describe('stream slice', () => { describe('loadConsumerGroupsSuccess', () => { it('should properly set groups.data = payload', () => { // Arrange + + console.log('Date.now()', Date.now()) + const data: ConsumerGroupDto[] = [{ name: '123', consumers: 123, From 0960e72bd8cc5e578940dc81ac8508684386c9e1 Mon Sep 17 00:00:00 2001 From: "Roman.Sergeenko" Date: Fri, 27 May 2022 16:03:54 +0400 Subject: [PATCH 17/20] #RI-2936 - integrations with list --- redisinsight/ui/src/constants/texts.tsx | 9 ++ .../add-stream-group/AddStreamGroup.tsx | 10 +- .../groups-view/GroupsView/styles.module.scss | 12 +++ .../groups-view/GroupsViewWrapper.tsx | 94 +++++++++++-------- redisinsight/ui/src/slices/browser/stream.ts | 25 ++++- 5 files changed, 99 insertions(+), 51 deletions(-) diff --git a/redisinsight/ui/src/constants/texts.tsx b/redisinsight/ui/src/constants/texts.tsx index 9fabc85d26..afc5413a39 100644 --- a/redisinsight/ui/src/constants/texts.tsx +++ b/redisinsight/ui/src/constants/texts.tsx @@ -25,3 +25,12 @@ export const ScanNoResultsFoundText = ( ) + +export const lastDeliveredIDTooltipText = ( + <> + Specify the ID of the last delivered entry in the stream from the new group's perspective. + + Otherwise, $ represents the ID of the last entry in the stream,  + 0 fetches the entire stream from the beginning. + +) diff --git a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/AddStreamGroup.tsx b/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/AddStreamGroup.tsx index d1e3efbc5f..47d1c72bc0 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/AddStreamGroup.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/AddStreamGroup.tsx @@ -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' @@ -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. - - Otherwise, $ represents the ID of the last entry in the stream,  - 0 fetches the entire stream from the beginning. - - )} + content={lastDeliveredIDTooltipText} > diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/styles.module.scss b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/styles.module.scss index e553560817..032ae48f64 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/styles.module.scss @@ -18,3 +18,15 @@ .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); +} diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsViewWrapper.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsViewWrapper.tsx index edf410170d..390c18aeba 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsViewWrapper.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsViewWrapper.tsx @@ -1,8 +1,9 @@ -import { EuiFieldText, EuiIcon, EuiSpacer, 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 { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys' +import { lastDeliveredIDTooltipText } from 'uiSrc/constants/texts' +import { selectedKeyDataSelector, updateSelectedKeyRefreshTime } from 'uiSrc/slices/browser/keys' import { streamGroupsSelector, @@ -13,12 +14,13 @@ import { } 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 { 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' @@ -39,15 +41,18 @@ const GroupsViewWrapper = (props: Props) => { const { lastRefreshTime, data: loadedGroups = [], + loading } = useSelector(streamGroupsSelector) const { name: key = '' } = useSelector(connectedInstanceSelector) - const { name: selectedKey } = useSelector(selectedKeyDataSelector) + const { name: selectedKey } = useSelector(selectedKeyDataSelector) ?? {} const dispatch = useDispatch() const [groups, setGroups] = useState([]) const [deleting, setDeleting] = useState('') const [editValue, setEditValue] = useState('') + const [idError, setIdError] = useState('') + const [isIdFocused, setIsIdFocused] = useState(false) useEffect(() => { dispatch(updateSelectedKeyRefreshTime(lastRefreshTime)) @@ -62,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('') }, []) @@ -98,12 +111,14 @@ const GroupsViewWrapper = (props: Props) => { } const handleApplyEditId = (groupName: string) => { - const data = { - keyName: selectedKey, - name: groupName, - lastDeliveredId: editValue + if (!!groupName.length && !idError && selectedKey) { + const data: UpdateConsumerGroupDto = { + keyName: selectedKey, + name: groupName, + lastDeliveredId: editValue + } + dispatch(modifyLastDeliveredIdAction(data)) } - dispatch(modifyLastDeliveredIdAction(data)) } const columns: ITableColumn[] = [ @@ -193,6 +208,7 @@ const GroupsViewWrapper = (props: Props) => { maxWidth: actionsWidth, minWidth: actionsWidth, render: function Actions(_act: any, { lastDeliveredId, name }: ConsumerGroupDto) { + const showIdError = !isIdFocused && idError return (
{ onOpen={() => setEditValue(lastDeliveredId)} onApply={() => handleApplyEditId(name)} className={styles.editLastId} + isDisabled={!editValue.length || !!idError} + isLoading={loading} > - setEditValue(e.target.value)} - append={( - - Specify the ID of the last delivered entry in the stream from the new group's perspective. - - Otherwise, $ represents the ID of the last entry in the stream,  - 0 fetches the entire stream from the beginning. - - )} - > - - - )} - style={{ width: 240 }} - autoComplete="off" - data-testid="id-field" - /> + <> + setEditValue(e.target.value)} + onBlur={() => setIsIdFocused(false)} + onFocus={() => setIsIdFocused(true)} + append={( + + + + )} + style={{ width: 240 }} + autoComplete="off" + data-testid="last-id-field" + /> + {!showIdError && Timestamp - Sequence Number or $} + {showIdError && {idError}} + { + state.groups.loading = true + }, + + modifyLastDeliveredIdSuccess: (state) => { + state.groups.loading = false + }, + + modifyLastDeliveredIdFailure: (state, { payload }) => { + state.groups.loading = false + state.groups.error = payload + }, + setSelectedConsumer: (state, { payload }) => { state.groups.selectedGroup = { ...state.groups.selectedGroup, @@ -248,6 +262,9 @@ export const { loadConsumerGroups, loadConsumerGroupsSuccess, loadConsumerGroupsFailure, + modifyLastDeliveredId, + modifyLastDeliveredIdSuccess, + modifyLastDeliveredIdFailure, loadConsumersSuccess, loadConsumersFailure, loadConsumerMessagesSuccess, @@ -643,12 +660,12 @@ export function fetchConsumerMessages( // Asynchronous thunk action export function modifyLastDeliveredIdAction( - data: any, + data: UpdateConsumerGroupDto, onSuccess?: () => void, onFailed?: () => void ) { return async (dispatch: AppDispatch, stateInit: () => RootState) => { - //dispatch(addNewEntries()) + dispatch(modifyLastDeliveredId()) try { const state = stateInit() @@ -662,7 +679,7 @@ export function modifyLastDeliveredIdAction( ) if (isStatusSuccessful(status)) { - //dispatch(addNewEntriesSuccess()) + dispatch(modifyLastDeliveredIdSuccess()) dispatch(fetchConsumerGroups(false)) keyName && dispatch(refreshKeyInfoAction(keyName)) onSuccess?.() @@ -671,7 +688,7 @@ export function modifyLastDeliveredIdAction( const error = _err as AxiosError const errorMessage = getApiErrorMessage(error) dispatch(addErrorNotification(error)) - //dispatch(addNewEntriesFailure(errorMessage)) + dispatch(modifyLastDeliveredIdFailure(errorMessage)) onFailed?.() } } From 32071c028a2d1645ecf9efda6e6589f9e7351990 Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Fri, 27 May 2022 16:41:30 +0400 Subject: [PATCH 18/20] #RI-2932 - fix pr comments --- .../key-details/KeyDetailsWrapper.tsx | 19 ++------------ .../StreamDataView/StreamDataView.tsx | 7 +---- .../StreamDataViewWrapper.tsx | 26 +++---------------- .../stream-details/stream-tabs/StreamTabs.tsx | 1 + redisinsight/ui/src/slices/browser/keys.ts | 2 -- redisinsight/ui/src/slices/browser/stream.ts | 24 +++++++++++++++++ 6 files changed, 31 insertions(+), 48 deletions(-) diff --git a/redisinsight/ui/src/pages/browser/components/key-details/KeyDetailsWrapper.tsx b/redisinsight/ui/src/pages/browser/components/key-details/KeyDetailsWrapper.tsx index 95cdc35e92..3836ad9523 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details/KeyDetailsWrapper.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-details/KeyDetailsWrapper.tsx @@ -16,13 +16,9 @@ import { refreshSetMembersAction } from 'uiSrc/slices/browser/set' import { refreshListElementsAction } from 'uiSrc/slices/browser/list' import { fetchReJSON } from 'uiSrc/slices/browser/rejson' import { - fetchConsumerGroups, - fetchConsumerMessages, - fetchConsumers, - refreshStreamEntries, + refreshStream, streamSelector, } from 'uiSrc/slices/browser/stream' -import { StreamViewType } from 'uiSrc/slices/interfaces/stream' import KeyDetails from './KeyDetails/KeyDetails' export interface Props { @@ -99,18 +95,7 @@ const KeyDetailsWrapper = (props: Props) => { break } case KeyTypes.Stream: { - if (streamViewType === StreamViewType.Data) { - dispatch(refreshStreamEntries(key, resetData)) - } - if (streamViewType === StreamViewType.Groups) { - dispatch(fetchConsumerGroups(resetData)) - } - if (streamViewType === StreamViewType.Consumers) { - dispatch(fetchConsumers(resetData)) - } - if (streamViewType === StreamViewType.Messages) { - dispatch(fetchConsumerMessages(resetData)) - } + dispatch(refreshStream(key, resetData)) break } default: diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView/StreamDataView.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView/StreamDataView.tsx index 9c300a4279..0ebe017ec6 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView/StreamDataView.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataView/StreamDataView.tsx @@ -24,14 +24,9 @@ const minColumnWidth = 190 const noItemsMessageInEmptyStream = 'There are no Entries in the Stream.' const noItemsMessageInRange = 'No results found.' -interface IStreamEntry extends StreamEntryDto { - editing: boolean -} - export interface Props { - data: IStreamEntry[] + data: StreamEntryDto[] columns: ITableColumn[] - onEditEntry: (entryId:string, editing: boolean) => void onClosePopover: () => void loadMoreItems: () => void isFooterOpen?: boolean diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx index 2764dd3754..5e9a339698 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx @@ -17,10 +17,6 @@ import { StreamEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' import StreamDataView from './StreamDataView' import styles from './StreamDataView/styles.module.scss' -export interface IStreamEntry extends StreamEntryDto { - editing: boolean -} - const suffix = '_stream' const actionsWidth = 50 const minColumnWidth = 190 @@ -43,7 +39,7 @@ const StreamDataViewWrapper = (props: Props) => { // for Manager columns // const [uniqFields, setUniqFields] = useState({}) - const [entries, setEntries] = useState([]) + const [entries, setEntries] = useState([]) const [columns, setColumns] = useState([]) const [deleting, setDeleting] = useState('') @@ -53,21 +49,16 @@ const StreamDataViewWrapper = (props: Props) => { useEffect(() => { let fields = {} - const streamEntries: IStreamEntry[] = loadedEntries?.map((item) => { + loadedEntries?.forEach((item) => { fields = { ...fields, ...keyBy(Object.keys(item.fields)) } - - return { - ...item, - editing: false, - } }) // for Manager columns // setUniqFields(fields) - setEntries(streamEntries) + setEntries(loadedEntries) setColumns([idColumn, ...Object.keys(fields).map((field) => getTemplateColumn(field)), actionsColumn]) }, [loadedEntries, deleting]) @@ -113,16 +104,6 @@ const StreamDataViewWrapper = (props: Props) => { }) } - const handleEditEntry = (entryId = '', editing: boolean) => { - const newFieldsState = entries.map((item) => { - if (item.id === entryId) { - return { ...item, editing } - } - return item - }) - setEntries(newFieldsState) - } - const getTemplateColumn = (label: string) : ITableColumn => ({ id: label, label, @@ -225,7 +206,6 @@ const StreamDataViewWrapper = (props: Props) => { diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.tsx index 14ad3ce2be..6f8ec2f6d5 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.tsx @@ -51,6 +51,7 @@ const StreamTabs = () => { isSelected={viewType === id} onClick={() => onSelectedTabChanged(id)} key={id} + data-testid={`stream-tab-${id}`} > {label} diff --git a/redisinsight/ui/src/slices/browser/keys.ts b/redisinsight/ui/src/slices/browser/keys.ts index a09dbd8f64..d01d1d84a1 100644 --- a/redisinsight/ui/src/slices/browser/keys.ts +++ b/redisinsight/ui/src/slices/browser/keys.ts @@ -558,8 +558,6 @@ export function fetchKeyInfo(key: string, resetData?: boolean) { SortOrder.DESC, resetData )) - } else if (viewType === StreamViewType.Groups) { - // dispatch(fetchConsumerGroups()) } } } catch (_err) { diff --git a/redisinsight/ui/src/slices/browser/stream.ts b/redisinsight/ui/src/slices/browser/stream.ts index 9658f0ba4c..d477f619aa 100644 --- a/redisinsight/ui/src/slices/browser/stream.ts +++ b/redisinsight/ui/src/slices/browser/stream.ts @@ -323,6 +323,30 @@ export function fetchStreamEntries( } } +// Asynchronous thunk action +export function refreshStream( + key: string, + resetData: boolean = false, +) { + return async (dispatch: AppDispatch, stateInit: () => RootState) => { + const state = stateInit() + const streamViewType = state.browser.stream.viewType + + if (streamViewType === StreamViewType.Data) { + dispatch(refreshStreamEntries(key, resetData)) + } + if (streamViewType === StreamViewType.Groups) { + dispatch(fetchConsumerGroups(resetData)) + } + if (streamViewType === StreamViewType.Consumers) { + dispatch(fetchConsumers(resetData)) + } + if (streamViewType === StreamViewType.Messages) { + dispatch(fetchConsumerMessages(resetData)) + } + } +} + // Asynchronous thunk action export function refreshStreamEntries( key: string, From 37fcc1cd9dccbb8712b695ea9d4fee23432d76cc Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Fri, 27 May 2022 16:46:41 +0400 Subject: [PATCH 19/20] fix text remove entry --- .../stream-data-view/StreamDataViewWrapper.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx index 5e9a339698..b77a3d17bc 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx @@ -180,9 +180,9 @@ const StreamDataViewWrapper = (props: Props) => { - Entry {id} will be removed from -
- {key} + will be removed from + {' '} + {key} )} item={id} From b9fbd1482a6110060be4aff516f6aa24212b491a Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Fri, 27 May 2022 16:50:54 +0400 Subject: [PATCH 20/20] fix text remove entry --- .../stream-data-view/StreamDataViewWrapper.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx index 5e9a339698..7097b80e89 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx @@ -178,11 +178,12 @@ const StreamDataViewWrapper = (props: Props) => { return (
- Entry {id} will be removed from -
- {key} + will be removed from + {' '} + {key} )} item={id}