diff --git a/redisinsight/ui/src/constants/api.ts b/redisinsight/ui/src/constants/api.ts
index f70a75687e..bbe3cff35a 100644
--- a/redisinsight/ui/src/constants/api.ts
+++ b/redisinsight/ui/src/constants/api.ts
@@ -37,6 +37,7 @@ enum ApiEndpoints {
STREAMS_ENTRIES_GET = 'streams/entries/get',
STREAMS_CONSUMER_GROUPS = 'streams/consumer-groups',
STREAMS_CONSUMER_GROUPS_GET = 'streams/consumer-groups/get',
+ STREAMS_CONSUMERS = 'streams/consumer-groups/consumers',
STREAMS_CONSUMERS_GET = 'streams/consumer-groups/consumers/get',
STREAMS_CONSUMERS_MESSAGES_GET = 'streams/consumer-groups/consumers/pending-messages/get',
STREAMS = 'streams',
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 755fbe5fc9..ec56e59fea 100644
--- a/redisinsight/ui/src/pages/browser/components/popover-delete/PopoverDelete.tsx
+++ b/redisinsight/ui/src/pages/browser/components/popover-delete/PopoverDelete.tsx
@@ -63,6 +63,7 @@ const PopoverDelete = (props: Props) => {
data-testid={testid ? `${testid}-icon` : 'remove-icon'}
/>
)}
+ onClick={(e) => e.stopPropagation()}
>
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 2dc6f23b15..891b54f764 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
@@ -2,7 +2,11 @@ import React from 'react'
import { instance, mock } from 'ts-mockito'
import { cloneDeep } from 'lodash'
import { cleanup, fireEvent, mockedStore, render, screen } from 'uiSrc/utils/test-utils'
-import { loadConsumerGroups, setSelectedConsumer } from 'uiSrc/slices/browser/stream'
+import {
+ deleteConsumers,
+ 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'
@@ -77,4 +81,15 @@ describe('ConsumersViewWrapper', () => {
expect(store.getActions()).toEqual([...afterRenderActions, setSelectedConsumer(), loadConsumerGroups(false)])
})
+
+ it('should delete Consumer', () => {
+ render()
+
+ const afterRenderActions = [...store.getActions()]
+
+ fireEvent.click(screen.getByTestId('remove-consumer-button-test-icon'))
+ fireEvent.click(screen.getByTestId('remove-consumer-button-test'))
+
+ expect(store.getActions()).toEqual([...afterRenderActions, deleteConsumers()])
+ })
})
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 b5f15f4df4..bf87fa0fb4 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
@@ -2,19 +2,18 @@ import React, { useCallback, useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import {
- deleteStreamEntry,
setStreamViewType,
selectedGroupSelector,
setSelectedConsumer,
- fetchConsumerMessages
+ fetchConsumerMessages,
+ deleteConsumersAction
} from 'uiSrc/slices/browser/stream'
import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces'
import PopoverDelete from 'uiSrc/pages/browser/components/popover-delete/PopoverDelete'
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 { selectedKeyDataSelector, updateSelectedKeyRefreshTime } from 'uiSrc/slices/browser/keys'
import { ConsumerDto } from 'apiSrc/modules/browser/dto/stream.dto'
import ConsumersView from './ConsumersView'
@@ -29,8 +28,9 @@ export interface Props {
}
const ConsumersViewWrapper = (props: Props) => {
- const { name: key = '' } = useSelector(connectedInstanceSelector)
+ const { name: key = '' } = useSelector(selectedKeyDataSelector) ?? { name: '' }
const {
+ name: selectedGroupName = '',
lastRefreshTime,
data: loadedConsumers = [],
} = useSelector(selectedGroupSelector) ?? {}
@@ -52,7 +52,7 @@ const ConsumersViewWrapper = (props: Props) => {
}, [])
const handleDeleteConsumer = (consumerName = '') => {
- dispatch(deleteStreamEntry(key, [consumerName]))
+ dispatch(deleteConsumersAction(key, selectedGroupName, [consumerName]))
closePopover()
}
@@ -120,11 +120,10 @@ const ConsumersViewWrapper = (props: Props) => {
return (
- Consumer will be removed from
-
- {key}
+ will be removed from Consumer Group {selectedGroupName}
>
)}
item={name}
@@ -134,7 +133,7 @@ const ConsumersViewWrapper = (props: Props) => {
updateLoading={false}
showPopover={showPopover}
testid={`remove-consumer-button-${name}`}
- handleDeleteItem={handleDeleteConsumer}
+ handleDeleteItem={() => handleDeleteConsumer(name)}
handleButtonClick={handleRemoveIconClick}
/>
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 4bc50c1dd3..88ae482f68 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
@@ -2,7 +2,11 @@ import React from 'react'
import { instance, mock } from 'ts-mockito'
import { cloneDeep } from 'lodash'
import { cleanup, fireEvent, mockedStore, render, screen } from 'uiSrc/utils/test-utils'
-import { loadConsumerGroups, setSelectedGroup } from 'uiSrc/slices/browser/stream'
+import {
+ deleteConsumerGroups,
+ 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'
@@ -83,4 +87,15 @@ describe('GroupsViewWrapper', () => {
expect(store.getActions()).toEqual([...afterRenderActions, setSelectedGroup(), loadConsumerGroups(false)])
})
+
+ it('should delete Group', () => {
+ render()
+
+ const afterRenderActions = [...store.getActions()]
+
+ fireEvent.click(screen.getByTestId('remove-groups-button-test-icon'))
+ fireEvent.click(screen.getByTestId('remove-groups-button-test'))
+
+ expect(store.getActions()).toEqual([...afterRenderActions, deleteConsumerGroups()])
+ })
})
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 4af1329833..fd16b2b9ee 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
@@ -11,13 +11,13 @@ import {
fetchConsumers,
setStreamViewType,
modifyLastDeliveredIdAction,
+ deleteConsumerGroupsAction,
} 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, validateConsumerGroupId } from 'uiSrc/utils'
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 { ConsumerGroupDto, UpdateConsumerGroupDto } from 'apiSrc/modules/browser/dto/stream.dto'
@@ -43,8 +43,7 @@ const GroupsViewWrapper = (props: Props) => {
data: loadedGroups = [],
loading
} = useSelector(streamGroupsSelector)
- const { name: key = '' } = useSelector(connectedInstanceSelector)
- const { name: selectedKey } = useSelector(selectedKeyDataSelector) ?? {}
+ const { name: selectedKey } = useSelector(selectedKeyDataSelector) ?? { name: '' }
const dispatch = useDispatch()
@@ -83,8 +82,8 @@ const GroupsViewWrapper = (props: Props) => {
setDeleting(`${groupName + suffix}`)
}, [])
- const handleDeleteGroup = () => {
- // dispatch(deleteStreamEntry(key, [groupName]))
+ const handleDeleteGroup = (name: string) => {
+ dispatch(deleteConsumerGroupsAction(selectedKey, [name]))
closePopover()
}
@@ -248,11 +247,10 @@ const GroupsViewWrapper = (props: Props) => {
>
- Group will be removed from
-
- {key}
+ will be removed from {selectedKey}
>
)}
item={name}
@@ -262,7 +260,7 @@ const GroupsViewWrapper = (props: Props) => {
updateLoading={false}
showPopover={showPopover}
testid={`remove-groups-button-${name}`}
- handleDeleteItem={handleDeleteGroup}
+ handleDeleteItem={() => handleDeleteGroup(name)}
handleButtonClick={handleRemoveIconClick}
/>
diff --git a/redisinsight/ui/src/slices/browser/stream.ts b/redisinsight/ui/src/slices/browser/stream.ts
index b98a664014..efb51c8a62 100644
--- a/redisinsight/ui/src/slices/browser/stream.ts
+++ b/redisinsight/ui/src/slices/browser/stream.ts
@@ -168,6 +168,21 @@ const streamSlice = createSlice({
state.groups.loading = false
state.groups.error = payload
},
+
+ deleteConsumerGroups: (state) => {
+ state.groups.loading = true
+ state.groups.error = ''
+ },
+
+ deleteConsumerGroupsSuccess: (state) => {
+ state.groups.loading = false
+ },
+
+ deleteConsumerGroupsFailure: (state, { payload }) => {
+ state.groups.loading = false
+ state.groups.error = payload
+ },
+
setSelectedGroup: (state, { payload }) => {
state.groups.selectedGroup = payload
},
@@ -208,6 +223,20 @@ const streamSlice = createSlice({
state.viewType = StreamViewType.Groups
},
+ deleteConsumers: (state) => {
+ state.groups.loading = true
+ state.groups.error = ''
+ },
+
+ deleteConsumersSuccess: (state) => {
+ state.groups.loading = false
+ },
+
+ deleteConsumersFailure: (state, { payload }) => {
+ state.groups.loading = false
+ state.groups.error = payload
+ },
+
loadConsumerMessagesSuccess: (state, { payload }: PayloadAction) => {
state.groups.loading = false
@@ -262,11 +291,17 @@ export const {
loadConsumerGroups,
loadConsumerGroupsSuccess,
loadConsumerGroupsFailure,
+ deleteConsumerGroups,
+ deleteConsumerGroupsSuccess,
+ deleteConsumerGroupsFailure,
modifyLastDeliveredId,
modifyLastDeliveredIdSuccess,
modifyLastDeliveredIdFailure,
loadConsumersSuccess,
loadConsumersFailure,
+ deleteConsumers,
+ deleteConsumersSuccess,
+ deleteConsumersFailure,
loadConsumerMessagesSuccess,
loadConsumerMessagesFailure,
setSelectedGroup,
@@ -606,6 +641,44 @@ export function fetchConsumerGroups(
}
}
+export function deleteConsumerGroupsAction(keyName: string, consumerGroups: string[], onSuccessAction?: () => void) {
+ return async (dispatch: AppDispatch, stateInit: () => RootState) => {
+ dispatch(deleteConsumerGroups())
+ try {
+ const state = stateInit()
+ const { status } = await apiService.delete(
+ getUrl(
+ state.connections.instances.connectedInstance?.id,
+ ApiEndpoints.STREAMS_CONSUMER_GROUPS
+ ),
+ {
+ data: {
+ keyName,
+ consumerGroups,
+ },
+ }
+ )
+ if (isStatusSuccessful(status)) {
+ onSuccessAction?.()
+ dispatch(deleteConsumerGroupsSuccess())
+ dispatch(fetchConsumerGroups(false))
+ dispatch(refreshKeyInfoAction(keyName))
+ dispatch(addMessageNotification(
+ successMessages.REMOVED_KEY_VALUE(
+ keyName,
+ consumerGroups.join(''),
+ 'Group'
+ )
+ ))
+ }
+ } catch (error) {
+ const errorMessage = getApiErrorMessage(error)
+ dispatch(addErrorNotification(error))
+ dispatch(deleteConsumerGroupsFailure(errorMessage))
+ }
+ }
+}
+
// Asynchronous thunk action
export function fetchConsumers(
resetData?: boolean,
@@ -630,7 +703,7 @@ export function fetchConsumers(
if (isStatusSuccessful(status)) {
dispatch(loadConsumersSuccess(data))
- onSuccess?.(data)
+ onSuccess?.()
}
} catch (_err) {
if (!axios.isCancel(_err)) {
@@ -644,6 +717,51 @@ export function fetchConsumers(
}
}
+// Asynchronous thunk action
+export function deleteConsumersAction(
+ keyName: string,
+ groupName: string,
+ consumerNames: string[],
+ onSuccessAction?: () => void
+) {
+ return async (dispatch: AppDispatch, stateInit: () => RootState) => {
+ dispatch(deleteConsumers())
+ try {
+ const state = stateInit()
+ const { status } = await apiService.delete(
+ getUrl(
+ state.connections.instances.connectedInstance?.id,
+ ApiEndpoints.STREAMS_CONSUMERS
+ ),
+ {
+ data: {
+ keyName,
+ groupName,
+ consumerNames,
+ },
+ }
+ )
+ if (isStatusSuccessful(status)) {
+ onSuccessAction?.()
+ dispatch(deleteConsumersSuccess())
+ dispatch(fetchConsumers(false))
+ dispatch(refreshKeyInfoAction(keyName))
+ dispatch(addMessageNotification(
+ successMessages.REMOVED_KEY_VALUE(
+ keyName,
+ consumerNames.join(''),
+ 'Consumer'
+ )
+ ))
+ }
+ } catch (error) {
+ const errorMessage = getApiErrorMessage(error)
+ dispatch(addErrorNotification(error))
+ dispatch(deleteConsumersFailure(errorMessage))
+ }
+ }
+}
+
// Asynchronous thunk action
export function fetchConsumerMessages(
resetData?: boolean,
diff --git a/redisinsight/ui/src/slices/tests/browser/stream.spec.ts b/redisinsight/ui/src/slices/tests/browser/stream.spec.ts
index dae57229a1..59ab448deb 100644
--- a/redisinsight/ui/src/slices/tests/browser/stream.spec.ts
+++ b/redisinsight/ui/src/slices/tests/browser/stream.spec.ts
@@ -1,8 +1,10 @@
import { ConsumerDto, ConsumerGroupDto, PendingEntryDto } from 'apiSrc/modules/browser/dto/stream.dto'
import { AxiosError } from 'axios'
import { cloneDeep, omit } from 'lodash'
+import successMessages from 'uiSrc/components/notifications/success-messages'
import { SortOrder } from 'uiSrc/constants'
import { apiService } from 'uiSrc/services'
+import { refreshKeyInfo } from 'uiSrc/slices/browser/keys'
import reducer, {
initialState,
setStreamInitialState,
@@ -35,21 +37,21 @@ import reducer, {
loadConsumerMessagesFailure,
setSelectedGroup,
setSelectedConsumer,
- streamDataSelector,
- streamGroupsSelector,
- streamGroupsDataSelector,
- selectedGroupSelector,
- selectedConsumerSelector,
- fetchMoreStreamEntries,
- addNewEntriesAction,
- deleteStreamEntry,
fetchConsumerGroups,
fetchConsumers,
fetchConsumerMessages,
+ deleteConsumerGroups,
+ deleteConsumerGroupsAction,
+ deleteConsumerGroupsSuccess,
+ deleteConsumerGroupsFailure,
+ deleteConsumersAction,
+ deleteConsumers,
+ deleteConsumersSuccess,
+ deleteConsumersFailure,
} from 'uiSrc/slices/browser/stream'
import { StreamViewType } from 'uiSrc/slices/interfaces/stream'
import { cleanup, initialStateDefault, mockedStore, } from 'uiSrc/utils/test-utils'
-import { addErrorNotification } from '../../app/notifications'
+import { addErrorNotification, addMessageNotification } from '../../app/notifications'
jest.mock('uiSrc/services')
@@ -509,9 +511,6 @@ 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,
@@ -963,5 +962,129 @@ describe('stream slice', () => {
expect(store.getActions()).toEqual(expectedActions)
})
})
+
+ describe('deleteConsumerGroupsAction', () => {
+ it('succeed to delete data', async () => {
+ // Arrange
+ const keyName = 'key'
+ const groups = ['group']
+ const responsePayload = { status: 200 }
+
+ apiService.delete = jest.fn().mockResolvedValue(responsePayload)
+
+ const responsePayloadPost = { data: mockConsumers, status: 200 }
+
+ apiService.post = jest.fn().mockResolvedValue(responsePayloadPost)
+
+ // Act
+ await store.dispatch(deleteConsumerGroupsAction(keyName, groups))
+
+ // Assert
+ const expectedActions = [
+ deleteConsumerGroups(),
+ deleteConsumerGroupsSuccess(),
+ loadConsumerGroups(false),
+ refreshKeyInfo(),
+ addMessageNotification(
+ successMessages.REMOVED_KEY_VALUE(
+ keyName,
+ groups.join(''),
+ 'Group'
+ )
+ )
+ ]
+
+ expect(store.getActions()).toEqual(expectedActions)
+ })
+
+ it('failed to delete data', async () => {
+ const errorMessage = 'Something was wrong!'
+ const keyName = 'key'
+ const groups = ['group']
+ const responsePayload = {
+ response: {
+ status: 500,
+ data: { message: errorMessage },
+ },
+ }
+
+ apiService.delete = jest.fn().mockRejectedValue(responsePayload)
+
+ // Act
+ await store.dispatch(deleteConsumerGroupsAction(keyName, groups))
+
+ // Assert
+ const expectedActions = [
+ deleteConsumerGroups(),
+ addErrorNotification(responsePayload as AxiosError),
+ deleteConsumerGroupsFailure(errorMessage)
+ ]
+
+ expect(store.getActions()).toEqual(expectedActions)
+ })
+ })
+
+ describe('deleteConsumersAction', () => {
+ it('succeed to delete data', async () => {
+ // Arrange
+ const keyName = 'key'
+ const groupName = 'group'
+ const consumerNames = ['consumer']
+ const responsePayload = { status: 200 }
+
+ apiService.delete = jest.fn().mockResolvedValue(responsePayload)
+
+ const responsePayloadPost = { data: mockConsumers, status: 200 }
+
+ apiService.post = jest.fn().mockResolvedValue(responsePayloadPost)
+
+ // Act
+ await store.dispatch(deleteConsumersAction(keyName, groupName, consumerNames))
+
+ // Assert
+ const expectedActions = [
+ deleteConsumers(),
+ deleteConsumersSuccess(),
+ loadConsumerGroups(false),
+ refreshKeyInfo(),
+ addMessageNotification(
+ successMessages.REMOVED_KEY_VALUE(
+ keyName,
+ consumerNames.join(''),
+ 'Consumer'
+ )
+ )
+ ]
+
+ expect(store.getActions()).toEqual(expectedActions)
+ })
+
+ it('failed to delete data', async () => {
+ const errorMessage = 'Something was wrong!'
+ const keyName = 'key'
+ const groupName = 'group'
+ const consumerNames = ['consumer']
+ const responsePayload = {
+ response: {
+ status: 500,
+ data: { message: errorMessage },
+ },
+ }
+
+ apiService.delete = jest.fn().mockRejectedValue(responsePayload)
+
+ // Act
+ await store.dispatch(deleteConsumersAction(keyName, groupName, consumerNames))
+
+ // Assert
+ const expectedActions = [
+ deleteConsumers(),
+ addErrorNotification(responsePayload as AxiosError),
+ deleteConsumersFailure(errorMessage)
+ ]
+
+ expect(store.getActions()).toEqual(expectedActions)
+ })
+ })
})
})