diff --git a/redisinsight/api/src/modules/pub-sub/pub-sub.analytics.service.spec.ts b/redisinsight/api/src/modules/pub-sub/pub-sub.analytics.service.spec.ts index 99757afd2b..fc0df51d06 100644 --- a/redisinsight/api/src/modules/pub-sub/pub-sub.analytics.service.spec.ts +++ b/redisinsight/api/src/modules/pub-sub/pub-sub.analytics.service.spec.ts @@ -3,6 +3,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter'; import { mockDatabase } from 'src/__mocks__'; import { TelemetryEvents } from 'src/constants'; import { PubSubAnalyticsService } from './pub-sub.analytics.service'; +import { SubscriptionType } from './constants'; const instanceId = mockDatabase.id; @@ -45,15 +46,32 @@ describe('PubSubAnalyticsService', () => { }); describe('sendChannelSubscribeEvent', () => { - it('should emit sendChannelSubscribe event', () => { + it('should emit sendChannelSubscribe event for all channels', () => { service.sendChannelSubscribeEvent( instanceId, + [{ channel: '*', type: SubscriptionType.Subscribe }], ); expect(sendEventMethod).toHaveBeenCalledWith( TelemetryEvents.PubSubChannelSubscribed, { databaseId: instanceId, + allChannels: 'yes', + }, + ); + }); + + it('should emit sendChannelSubscribe event not for all channels', () => { + service.sendChannelSubscribeEvent( + instanceId, + [{ channel: '1', type: SubscriptionType.Subscribe }], + ); + + expect(sendEventMethod).toHaveBeenCalledWith( + TelemetryEvents.PubSubChannelSubscribed, + { + databaseId: instanceId, + allChannels: 'no', }, ); }); diff --git a/redisinsight/api/src/modules/pub-sub/pub-sub.analytics.service.ts b/redisinsight/api/src/modules/pub-sub/pub-sub.analytics.service.ts index b3c6cc64e8..0ff74f63c7 100644 --- a/redisinsight/api/src/modules/pub-sub/pub-sub.analytics.service.ts +++ b/redisinsight/api/src/modules/pub-sub/pub-sub.analytics.service.ts @@ -1,9 +1,11 @@ import { Injectable } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; -import { TelemetryEvents } from 'src/constants'; +import { some } from 'lodash'; +import { DEFAULT_MATCH, TelemetryEvents } from 'src/constants'; import { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service'; import { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto'; import { RedisError, ReplyError } from 'src/models'; +import { SubscriptionDto } from './dto'; export interface IExecResult { response: any; @@ -31,12 +33,13 @@ export class PubSubAnalyticsService extends TelemetryBaseService { } } - sendChannelSubscribeEvent(databaseId: string): void { + sendChannelSubscribeEvent(databaseId: string, subs: SubscriptionDto[]): void { try { this.sendEvent( TelemetryEvents.PubSubChannelSubscribed, { databaseId, + allChannels: some(subs, { channel: DEFAULT_MATCH }) ? 'yes' : 'no', }, ); } catch (e) { diff --git a/redisinsight/api/src/modules/pub-sub/pub-sub.service.ts b/redisinsight/api/src/modules/pub-sub/pub-sub.service.ts index 2aea95c19a..14195d65d3 100644 --- a/redisinsight/api/src/modules/pub-sub/pub-sub.service.ts +++ b/redisinsight/api/src/modules/pub-sub/pub-sub.service.ts @@ -34,7 +34,7 @@ export class PubSubService { await Promise.all(dto.subscriptions.map((subDto) => session.subscribe( this.subscriptionProvider.createSubscription(userClient, subDto), ))); - this.analyticsService.sendChannelSubscribeEvent(userClient.getDatabaseId()); + this.analyticsService.sendChannelSubscribeEvent(userClient.getDatabaseId(), dto.subscriptions); } catch (e) { this.logger.error('Unable to create subscriptions', e); diff --git a/redisinsight/ui/src/pages/pub-sub/PubSubPage.tsx b/redisinsight/ui/src/pages/pub-sub/PubSubPage.tsx index 39b2f25529..99239eb409 100644 --- a/redisinsight/ui/src/pages/pub-sub/PubSubPage.tsx +++ b/redisinsight/ui/src/pages/pub-sub/PubSubPage.tsx @@ -3,7 +3,6 @@ import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { useParams } from 'react-router-dom' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' -import { SubscriptionType } from 'uiSrc/constants/pubSub' import { sendEventTelemetry, sendPageViewTelemetry, TelemetryEvent, TelemetryPageView } from 'uiSrc/telemetry' import { formatLongName, getDbIndex, setTitle } from 'uiSrc/utils' @@ -15,8 +14,6 @@ import { MessagesListWrapper, PublishMessage, SubscriptionPanel } from './compon import styles from './styles.module.scss' -export const PUB_SUB_DEFAULT_CHANNEL = { channel: '*', type: SubscriptionType.PSubscribe } - const PubSubPage = () => { const { name: connectedInstanceName, db } = useSelector(connectedInstanceSelector) const { instanceId } = useParams<{ instanceId: string }>() diff --git a/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/SubscriptionPanel.spec.tsx b/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/SubscriptionPanel.spec.tsx new file mode 100644 index 0000000000..fd63fe312d --- /dev/null +++ b/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/SubscriptionPanel.spec.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import { fireEvent } from '@testing-library/react' +import { cloneDeep } from 'lodash' +import { toggleSubscribeTriggerPubSub } from 'uiSrc/slices/pubsub/pubsub' +import { cleanup, clearStoreActions, mockedStore, render, screen } from 'uiSrc/utils/test-utils' + +import SubscriptionPanel from './SubscriptionPanel' + +let store: typeof mockedStore + +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +describe('SubscriptionPanel', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should dispatch subscribe action after toggle subscribe button', () => { + render() + const expectedActions = [toggleSubscribeTriggerPubSub('1 2 3')] + fireEvent.change(screen.getByTestId('channels-input'), { target: { value: '1 2 3' } }) + fireEvent.click(screen.getByTestId('subscribe-btn')) + + expect(clearStoreActions(store.getActions())).toEqual(clearStoreActions(expectedActions)) + }) +}) diff --git a/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/SubscriptionPanel.tsx b/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/SubscriptionPanel.tsx index b9134fadb3..7086a56e5d 100644 --- a/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/SubscriptionPanel.tsx +++ b/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/SubscriptionPanel.tsx @@ -1,11 +1,10 @@ -import { EuiButton, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiToolTip } from '@elastic/eui' +import { EuiButton, EuiButtonIcon, EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiToolTip } from '@elastic/eui' import cx from 'classnames' -import React, { useContext } from 'react' +import React, { useContext, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { useParams } from 'react-router-dom' import { Theme } from 'uiSrc/constants' import { ThemeContext } from 'uiSrc/contexts/themeContext' -import { PUB_SUB_DEFAULT_CHANNEL } from 'uiSrc/pages/pub-sub/PubSubPage' import { clearPubSubMessages, pubSubSelector, toggleSubscribeTriggerPubSub } from 'uiSrc/slices/pubsub/pubsub' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' @@ -25,8 +24,10 @@ const SubscriptionPanel = () => { const { instanceId = '' } = useParams<{ instanceId: string }>() + const [channels, setChannels] = useState('') + const toggleSubscribe = () => { - dispatch(toggleSubscribeTriggerPubSub([PUB_SUB_DEFAULT_CHANNEL])) + dispatch(toggleSubscribeTriggerPubSub(channels)) } const onClickClear = () => { @@ -70,21 +71,17 @@ const SubscriptionPanel = () => { - {!!messages.length && ( - - - - - - )} + + setChannels(e.target.value)} + placeholder="Enter Channel to Subscribe" + aria-label="channel names for filtering" + data-testid="channels-input" + /> + { { isSubscribed ? 'Unsubscribe' : 'Subscribe' } + {!!messages.length && ( + + + + + + )} diff --git a/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/styles.module.scss b/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/styles.module.scss index e7b9089387..a1325738db 100644 --- a/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/styles.module.scss +++ b/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/styles.module.scss @@ -20,3 +20,15 @@ height: 18px; } } + +.channels { + margin-right: 8px; + + input { + width: 210px; + } + + :global(.euiFormControlLayout), input { + height: 30px; + } +} diff --git a/redisinsight/ui/src/slices/pubsub/pubsub.ts b/redisinsight/ui/src/slices/pubsub/pubsub.ts index 8ce4fc79c6..55b48b1321 100644 --- a/redisinsight/ui/src/slices/pubsub/pubsub.ts +++ b/redisinsight/ui/src/slices/pubsub/pubsub.ts @@ -6,7 +6,8 @@ import { addErrorNotification } from 'uiSrc/slices/app/notifications' import { StatePubSub } from 'uiSrc/slices/interfaces/pubsub' import { AppDispatch, RootState } from 'uiSrc/slices/store' import { getApiErrorMessage, getUrl, isStatusSuccessful } from 'uiSrc/utils' -import { SubscriptionDto } from 'apiSrc/modules/pub-sub/dto/subscription.dto' +import { DEFAULT_SEARCH_MATCH } from 'uiSrc/constants/api' +import { SubscriptionType } from 'uiSrc/constants/pubSub' import { MessagesResponse } from 'apiSrc/modules/pub-sub/dto/messages.response' import { PublishResponse } from 'apiSrc/modules/pub-sub/dto/publish.response' @@ -32,9 +33,15 @@ const pubSubSlice = createSlice({ setPubSubConnected: (state, { payload }: PayloadAction) => { state.isConnected = payload }, - toggleSubscribeTriggerPubSub: (state, { payload }: PayloadAction) => { + toggleSubscribeTriggerPubSub: (state, { payload }: PayloadAction) => { + const channels = payload.trim() || DEFAULT_SEARCH_MATCH + const subs = channels.split(' ').map((channel) => ({ + channel, + type: SubscriptionType.PSubscribe, + })) + state.isSubscribeTriggered = !state.isSubscribeTriggered - state.subscriptions = payload + state.subscriptions = subs }, setIsPubSubSubscribed: (state) => { state.isSubscribed = true diff --git a/redisinsight/ui/src/slices/tests/pubsub/pubsub.spec.ts b/redisinsight/ui/src/slices/tests/pubsub/pubsub.spec.ts index 0853c5367d..d1fb1a1ca5 100644 --- a/redisinsight/ui/src/slices/tests/pubsub/pubsub.spec.ts +++ b/redisinsight/ui/src/slices/tests/pubsub/pubsub.spec.ts @@ -64,17 +64,37 @@ describe('pubsub slice', () => { describe('toggleSubscribeTriggerPubSub', () => { it('should properly set state', () => { - const subscriptions = [{ channel: '1', type: 'ss' }] + const channels = '1 * 3' // Arrange const state = { ...initialState, isSubscribeTriggered: !initialState.isSubscribeTriggered, - subscriptions + subscriptions: [{ channel: '1', type: 'p' }, { channel: '*', type: 'p' }, { channel: '3', type: 'p' }] } // Act - const nextState = reducer(initialState, toggleSubscribeTriggerPubSub(subscriptions)) + const nextState = reducer(initialState, toggleSubscribeTriggerPubSub(channels)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + pubsub: nextState, + }) + expect(pubSubSelector(rootState)).toEqual(state) + }) + + it('should properly set state for empty channels', () => { + const channels = '' + + // Arrange + const state = { + ...initialState, + isSubscribeTriggered: !initialState.isSubscribeTriggered, + subscriptions: [{ channel: '*', type: 'p' }] + } + + // Act + const nextState = reducer(initialState, toggleSubscribeTriggerPubSub(channels)) // Assert const rootState = Object.assign(initialStateDefault, { diff --git a/tests/e2e/pageObjects/pub-sub-page.ts b/tests/e2e/pageObjects/pub-sub-page.ts index edcca0859d..84986cfc12 100644 --- a/tests/e2e/pageObjects/pub-sub-page.ts +++ b/tests/e2e/pageObjects/pub-sub-page.ts @@ -27,6 +27,7 @@ export class PubSubPage extends InstancePage { //INPUTS channelNameInput = Selector('[data-testid=field-channel-name]'); messageInput = Selector('[data-testid=field-message]'); + channelsSubscribeInput = Selector('[data-testid=channels-input]'); /** * Publish message in pubsub diff --git a/tests/e2e/tests/web/regression/pub-sub/pub-sub-oss-cluster-7.ts b/tests/e2e/tests/web/regression/pub-sub/pub-sub-oss-cluster-7.ts index a26998c679..1fbaffbbe3 100644 --- a/tests/e2e/tests/web/regression/pub-sub/pub-sub-oss-cluster-7.ts +++ b/tests/e2e/tests/web/regression/pub-sub/pub-sub-oss-cluster-7.ts @@ -12,19 +12,20 @@ const databaseAPIRequests = new DatabaseAPIRequests(); fixture `PubSub OSS Cluster 7 tests` .meta({ type: 'regression' }) - .page(commonUrl); + .page(commonUrl) -test - .before(async t => { + .beforeEach(async t => { await databaseHelper.acceptLicenseTerms(); await databaseAPIRequests.addNewOSSClusterDatabaseApi(ossClusterConfig); await myRedisDatabasePage.reloadPage(); await myRedisDatabasePage.clickOnDBByName(ossClusterConfig.ossClusterDatabaseName); await t.click(myRedisDatabasePage.NavigationPanel.pubSubButton); }) - .after(async() => { + .afterEach(async() => { await databaseAPIRequests.deleteOSSClusterDatabaseApi(ossClusterConfig); - }) + }); +test + .meta({ rte: rte.ossCluster })('Verify that SPUBLISH message is displayed for OSS Cluster 7 database', async t => { await t.expect(pubSubPage.ossClusterEmptyMessage.exists).ok('SPUBLISH message not displayed'); // Verify that user can see published messages for OSS Cluster 7 @@ -54,3 +55,16 @@ test await pubSubPage.Cli.sendCommandInCli('10 spublish channel oss_cluster_message_spublish'); await verifyMessageDisplayingInPubSub('oss_cluster_message_spublish', false); }); + +test.meta({ rte: rte.ossCluster })('Verify that PSUBSCRIBE works, that user can specify channel name to subscribe', async t => { + const channelsName = 'first second third'; + await t.typeText(pubSubPage.channelsSubscribeInput, channelsName, { replace: true }); + await t.click(pubSubPage.subscribeButton); + await t.expect(pubSubPage.channelsSubscribeInput.hasAttribute('disabled')).ok('the field is not disabled after subscribe'); + await pubSubPage.publishMessage(channelsName.split(' ')[0], 'published message'); + await verifyMessageDisplayingInPubSub('published message', true); + await pubSubPage.publishMessage(channelsName.split(' ')[1], 'second message'); + await verifyMessageDisplayingInPubSub('second message', true); + await pubSubPage.publishMessage('not exist', 'not exist message'); + await verifyMessageDisplayingInPubSub('not exist message', false); +});