Skip to content

Commit bac97a3

Browse files
Merge pull request #3832 from RedisInsight/feature/RI-6097_Subscript_to_specific_channel_pubsub
#RI-6097 - Subscribe to a specific pub/sub channel
2 parents b413a0e + 25d2c01 commit bac97a3

File tree

11 files changed

+151
-37
lines changed

11 files changed

+151
-37
lines changed

redisinsight/api/src/modules/pub-sub/pub-sub.analytics.service.spec.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
33
import { mockDatabase } from 'src/__mocks__';
44
import { TelemetryEvents } from 'src/constants';
55
import { PubSubAnalyticsService } from './pub-sub.analytics.service';
6+
import { SubscriptionType } from './constants';
67

78
const instanceId = mockDatabase.id;
89

@@ -45,15 +46,32 @@ describe('PubSubAnalyticsService', () => {
4546
});
4647

4748
describe('sendChannelSubscribeEvent', () => {
48-
it('should emit sendChannelSubscribe event', () => {
49+
it('should emit sendChannelSubscribe event for all channels', () => {
4950
service.sendChannelSubscribeEvent(
5051
instanceId,
52+
[{ channel: '*', type: SubscriptionType.Subscribe }],
5153
);
5254

5355
expect(sendEventMethod).toHaveBeenCalledWith(
5456
TelemetryEvents.PubSubChannelSubscribed,
5557
{
5658
databaseId: instanceId,
59+
allChannels: 'yes',
60+
},
61+
);
62+
});
63+
64+
it('should emit sendChannelSubscribe event not for all channels', () => {
65+
service.sendChannelSubscribeEvent(
66+
instanceId,
67+
[{ channel: '1', type: SubscriptionType.Subscribe }],
68+
);
69+
70+
expect(sendEventMethod).toHaveBeenCalledWith(
71+
TelemetryEvents.PubSubChannelSubscribed,
72+
{
73+
databaseId: instanceId,
74+
allChannels: 'no',
5775
},
5876
);
5977
});

redisinsight/api/src/modules/pub-sub/pub-sub.analytics.service.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { Injectable } from '@nestjs/common';
22
import { EventEmitter2 } from '@nestjs/event-emitter';
3-
import { TelemetryEvents } from 'src/constants';
3+
import { some } from 'lodash';
4+
import { DEFAULT_MATCH, TelemetryEvents } from 'src/constants';
45
import { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service';
56
import { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto';
67
import { RedisError, ReplyError } from 'src/models';
8+
import { SubscriptionDto } from './dto';
79

810
export interface IExecResult {
911
response: any;
@@ -31,12 +33,13 @@ export class PubSubAnalyticsService extends TelemetryBaseService {
3133
}
3234
}
3335

34-
sendChannelSubscribeEvent(databaseId: string): void {
36+
sendChannelSubscribeEvent(databaseId: string, subs: SubscriptionDto[]): void {
3537
try {
3638
this.sendEvent(
3739
TelemetryEvents.PubSubChannelSubscribed,
3840
{
3941
databaseId,
42+
allChannels: some(subs, { channel: DEFAULT_MATCH }) ? 'yes' : 'no',
4043
},
4144
);
4245
} catch (e) {

redisinsight/api/src/modules/pub-sub/pub-sub.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export class PubSubService {
3434
await Promise.all(dto.subscriptions.map((subDto) => session.subscribe(
3535
this.subscriptionProvider.createSubscription(userClient, subDto),
3636
)));
37-
this.analyticsService.sendChannelSubscribeEvent(userClient.getDatabaseId());
37+
this.analyticsService.sendChannelSubscribeEvent(userClient.getDatabaseId(), dto.subscriptions);
3838
} catch (e) {
3939
this.logger.error('Unable to create subscriptions', e);
4040

redisinsight/ui/src/pages/pub-sub/PubSubPage.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import React, { useEffect, useState } from 'react'
33
import { useDispatch, useSelector } from 'react-redux'
44
import { useParams } from 'react-router-dom'
55
import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'
6-
import { SubscriptionType } from 'uiSrc/constants/pubSub'
76
import { sendEventTelemetry, sendPageViewTelemetry, TelemetryEvent, TelemetryPageView } from 'uiSrc/telemetry'
87
import { formatLongName, getDbIndex, setTitle } from 'uiSrc/utils'
98

@@ -15,8 +14,6 @@ import { MessagesListWrapper, PublishMessage, SubscriptionPanel } from './compon
1514

1615
import styles from './styles.module.scss'
1716

18-
export const PUB_SUB_DEFAULT_CHANNEL = { channel: '*', type: SubscriptionType.PSubscribe }
19-
2017
const PubSubPage = () => {
2118
const { name: connectedInstanceName, db } = useSelector(connectedInstanceSelector)
2219
const { instanceId } = useParams<{ instanceId: string }>()
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React from 'react'
2+
import { fireEvent } from '@testing-library/react'
3+
import { cloneDeep } from 'lodash'
4+
import { toggleSubscribeTriggerPubSub } from 'uiSrc/slices/pubsub/pubsub'
5+
import { cleanup, clearStoreActions, mockedStore, render, screen } from 'uiSrc/utils/test-utils'
6+
7+
import SubscriptionPanel from './SubscriptionPanel'
8+
9+
let store: typeof mockedStore
10+
11+
beforeEach(() => {
12+
cleanup()
13+
store = cloneDeep(mockedStore)
14+
store.clearActions()
15+
})
16+
17+
describe('SubscriptionPanel', () => {
18+
it('should render', () => {
19+
expect(render(<SubscriptionPanel />)).toBeTruthy()
20+
})
21+
22+
it('should dispatch subscribe action after toggle subscribe button', () => {
23+
render(<SubscriptionPanel />)
24+
const expectedActions = [toggleSubscribeTriggerPubSub('1 2 3')]
25+
fireEvent.change(screen.getByTestId('channels-input'), { target: { value: '1 2 3' } })
26+
fireEvent.click(screen.getByTestId('subscribe-btn'))
27+
28+
expect(clearStoreActions(store.getActions())).toEqual(clearStoreActions(expectedActions))
29+
})
30+
})

redisinsight/ui/src/pages/pub-sub/components/subscription-panel/SubscriptionPanel.tsx

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import { EuiButton, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiToolTip } from '@elastic/eui'
1+
import { EuiButton, EuiButtonIcon, EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiToolTip } from '@elastic/eui'
22
import cx from 'classnames'
3-
import React, { useContext } from 'react'
3+
import React, { useContext, useState } from 'react'
44
import { useDispatch, useSelector } from 'react-redux'
55
import { useParams } from 'react-router-dom'
66
import { Theme } from 'uiSrc/constants'
77
import { ThemeContext } from 'uiSrc/contexts/themeContext'
8-
import { PUB_SUB_DEFAULT_CHANNEL } from 'uiSrc/pages/pub-sub/PubSubPage'
98
import { clearPubSubMessages, pubSubSelector, toggleSubscribeTriggerPubSub } from 'uiSrc/slices/pubsub/pubsub'
109
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
1110

@@ -25,8 +24,10 @@ const SubscriptionPanel = () => {
2524

2625
const { instanceId = '' } = useParams<{ instanceId: string }>()
2726

27+
const [channels, setChannels] = useState('')
28+
2829
const toggleSubscribe = () => {
29-
dispatch(toggleSubscribeTriggerPubSub([PUB_SUB_DEFAULT_CHANNEL]))
30+
dispatch(toggleSubscribeTriggerPubSub(channels))
3031
}
3132

3233
const onClickClear = () => {
@@ -70,21 +71,17 @@ const SubscriptionPanel = () => {
7071
</EuiFlexItem>
7172
<EuiFlexItem grow={false}>
7273
<EuiFlexGroup alignItems="center" gutterSize="none" responsive={false}>
73-
{!!messages.length && (
74-
<EuiFlexItem grow={false} style={{ marginRight: 12 }}>
75-
<EuiToolTip
76-
content="Clear Messages"
77-
anchorClassName={cx('inline-flex')}
78-
>
79-
<EuiButtonIcon
80-
iconType="eraser"
81-
onClick={onClickClear}
82-
aria-label="clear pub sub"
83-
data-testid="clear-pubsub-btn"
84-
/>
85-
</EuiToolTip>
86-
</EuiFlexItem>
87-
)}
74+
<EuiFlexItem grow={false} className={styles.channels}>
75+
<EuiFieldText
76+
value={channels}
77+
disabled={isSubscribed}
78+
className={styles.channels}
79+
onChange={(e) => setChannels(e.target.value)}
80+
placeholder="Enter Channel to Subscribe"
81+
aria-label="channel names for filtering"
82+
data-testid="channels-input"
83+
/>
84+
</EuiFlexItem>
8885
<EuiFlexItem grow={false}>
8986
<EuiButton
9087
fill={!isSubscribed}
@@ -100,6 +97,21 @@ const SubscriptionPanel = () => {
10097
{ isSubscribed ? 'Unsubscribe' : 'Subscribe' }
10198
</EuiButton>
10299
</EuiFlexItem>
100+
{!!messages.length && (
101+
<EuiFlexItem grow={false} style={{ marginLeft: 8 }}>
102+
<EuiToolTip
103+
content="Clear Messages"
104+
anchorClassName={cx('inline-flex')}
105+
>
106+
<EuiButtonIcon
107+
iconType="eraser"
108+
onClick={onClickClear}
109+
aria-label="clear pub sub"
110+
data-testid="clear-pubsub-btn"
111+
/>
112+
</EuiToolTip>
113+
</EuiFlexItem>
114+
)}
103115
</EuiFlexGroup>
104116
</EuiFlexItem>
105117
</EuiFlexGroup>

redisinsight/ui/src/pages/pub-sub/components/subscription-panel/styles.module.scss

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,15 @@
2020
height: 18px;
2121
}
2222
}
23+
24+
.channels {
25+
margin-right: 8px;
26+
27+
input {
28+
width: 210px;
29+
}
30+
31+
:global(.euiFormControlLayout), input {
32+
height: 30px;
33+
}
34+
}

redisinsight/ui/src/slices/pubsub/pubsub.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import { addErrorNotification } from 'uiSrc/slices/app/notifications'
66
import { StatePubSub } from 'uiSrc/slices/interfaces/pubsub'
77
import { AppDispatch, RootState } from 'uiSrc/slices/store'
88
import { getApiErrorMessage, getUrl, isStatusSuccessful } from 'uiSrc/utils'
9-
import { SubscriptionDto } from 'apiSrc/modules/pub-sub/dto/subscription.dto'
9+
import { DEFAULT_SEARCH_MATCH } from 'uiSrc/constants/api'
10+
import { SubscriptionType } from 'uiSrc/constants/pubSub'
1011
import { MessagesResponse } from 'apiSrc/modules/pub-sub/dto/messages.response'
1112
import { PublishResponse } from 'apiSrc/modules/pub-sub/dto/publish.response'
1213

@@ -32,9 +33,15 @@ const pubSubSlice = createSlice({
3233
setPubSubConnected: (state, { payload }: PayloadAction<boolean>) => {
3334
state.isConnected = payload
3435
},
35-
toggleSubscribeTriggerPubSub: (state, { payload }: PayloadAction<SubscriptionDto[]>) => {
36+
toggleSubscribeTriggerPubSub: (state, { payload }: PayloadAction<string>) => {
37+
const channels = payload.trim() || DEFAULT_SEARCH_MATCH
38+
const subs = channels.split(' ').map((channel) => ({
39+
channel,
40+
type: SubscriptionType.PSubscribe,
41+
}))
42+
3643
state.isSubscribeTriggered = !state.isSubscribeTriggered
37-
state.subscriptions = payload
44+
state.subscriptions = subs
3845
},
3946
setIsPubSubSubscribed: (state) => {
4047
state.isSubscribed = true

redisinsight/ui/src/slices/tests/pubsub/pubsub.spec.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,37 @@ describe('pubsub slice', () => {
6464

6565
describe('toggleSubscribeTriggerPubSub', () => {
6666
it('should properly set state', () => {
67-
const subscriptions = [{ channel: '1', type: 'ss' }]
67+
const channels = '1 * 3'
6868

6969
// Arrange
7070
const state = {
7171
...initialState,
7272
isSubscribeTriggered: !initialState.isSubscribeTriggered,
73-
subscriptions
73+
subscriptions: [{ channel: '1', type: 'p' }, { channel: '*', type: 'p' }, { channel: '3', type: 'p' }]
7474
}
7575

7676
// Act
77-
const nextState = reducer(initialState, toggleSubscribeTriggerPubSub(subscriptions))
77+
const nextState = reducer(initialState, toggleSubscribeTriggerPubSub(channels))
78+
79+
// Assert
80+
const rootState = Object.assign(initialStateDefault, {
81+
pubsub: nextState,
82+
})
83+
expect(pubSubSelector(rootState)).toEqual(state)
84+
})
85+
86+
it('should properly set state for empty channels', () => {
87+
const channels = ''
88+
89+
// Arrange
90+
const state = {
91+
...initialState,
92+
isSubscribeTriggered: !initialState.isSubscribeTriggered,
93+
subscriptions: [{ channel: '*', type: 'p' }]
94+
}
95+
96+
// Act
97+
const nextState = reducer(initialState, toggleSubscribeTriggerPubSub(channels))
7898

7999
// Assert
80100
const rootState = Object.assign(initialStateDefault, {

tests/e2e/pageObjects/pub-sub-page.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export class PubSubPage extends InstancePage {
2727
//INPUTS
2828
channelNameInput = Selector('[data-testid=field-channel-name]');
2929
messageInput = Selector('[data-testid=field-message]');
30+
channelsSubscribeInput = Selector('[data-testid=channels-input]');
3031

3132
/**
3233
* Publish message in pubsub

0 commit comments

Comments
 (0)