diff --git a/redisinsight/ui/src/assets/img/pub-sub/light-bulb.svg b/redisinsight/ui/src/assets/img/pub-sub/light-bulb.svg new file mode 100644 index 0000000000..7f0a69486b --- /dev/null +++ b/redisinsight/ui/src/assets/img/pub-sub/light-bulb.svg @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/redisinsight/ui/src/pages/pub-sub/PubSubPage.styles.tsx b/redisinsight/ui/src/pages/pub-sub/PubSubPage.styles.tsx new file mode 100644 index 0000000000..711bea6f36 --- /dev/null +++ b/redisinsight/ui/src/pages/pub-sub/PubSubPage.styles.tsx @@ -0,0 +1,9 @@ +import styled from 'styled-components' +import { Col } from 'uiSrc/components/base/layout/flex' + +export const OnboardingWrapper = styled(Col)` + align-items: flex-end; + /* Custom margin for onboarding popover */ + /* TODO: Rework the positioning of the onboarding container in order to remove this */ + margin-right: 28px; +` diff --git a/redisinsight/ui/src/pages/pub-sub/PubSubPage.tsx b/redisinsight/ui/src/pages/pub-sub/PubSubPage.tsx index 6809cdc9fd..d3216d62df 100644 --- a/redisinsight/ui/src/pages/pub-sub/PubSubPage.tsx +++ b/redisinsight/ui/src/pages/pub-sub/PubSubPage.tsx @@ -15,33 +15,15 @@ import { OnboardingTour } from 'uiSrc/components' import { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features' import { incrementOnboardStepAction } from 'uiSrc/slices/app/features' import { OnboardingSteps } from 'uiSrc/constants/onboarding' -import { - MessagesListWrapper, - PublishMessage, - SubscriptionPanel, -} from './components' - -import styles from './styles.module.scss' +import { MessagesListWrapper, PublishMessage } from './components' -// Styled components -const MainContainer = styled.div>` - border: 1px solid ${({ theme }) => theme.semantic.color.border.neutral500}; - border-radius: 8px; -` - -const ContentPanel = styled.div` - flex-grow: 1; -` +import { Col, FlexItem } from 'uiSrc/components/base/layout/flex' +import { Theme } from 'uiSrc/components/base/theme/types' +import { OnboardingWrapper } from './PubSubPage.styles' -const HeaderPanel = styled.div` - padding: 12px 18px; - border-bottom: 1px solid var(--separatorColor); - border-color: ${({ theme }) => theme.semantic.color.border.neutral500}; -` - -const FooterPanel = styled.div` - margin-top: 16px; - padding: 10px 18px 28px; +const FooterPanel = styled(FlexItem)` + border-top: 1px solid ${({ theme }) => theme.semantic.color.border.neutral500}; + padding: ${({ theme }: { theme: Theme }) => theme.core.space.space300}; ` const PubSubPage = () => { @@ -92,28 +74,24 @@ const PubSubPage = () => { } return ( - - - - - -
- -
-
- + + + + + + -
+ + -
-
+ + ) } diff --git a/redisinsight/ui/src/pages/pub-sub/components/index.ts b/redisinsight/ui/src/pages/pub-sub/components/index.ts index 913819eb3f..420cee4938 100644 --- a/redisinsight/ui/src/pages/pub-sub/components/index.ts +++ b/redisinsight/ui/src/pages/pub-sub/components/index.ts @@ -1,5 +1,4 @@ -import SubscriptionPanel from './subscription-panel' import MessagesListWrapper from './messages-list' import PublishMessage from './publish-message' -export { SubscriptionPanel, MessagesListWrapper, PublishMessage } +export { MessagesListWrapper, PublishMessage } diff --git a/redisinsight/ui/src/pages/pub-sub/components/messages-list/EmptyMessagesList/EmptyMessagesList.spec.tsx b/redisinsight/ui/src/pages/pub-sub/components/messages-list/EmptyMessagesList/EmptyMessagesList.spec.tsx index 36b018a561..2aea895bf8 100644 --- a/redisinsight/ui/src/pages/pub-sub/components/messages-list/EmptyMessagesList/EmptyMessagesList.spec.tsx +++ b/redisinsight/ui/src/pages/pub-sub/components/messages-list/EmptyMessagesList/EmptyMessagesList.spec.tsx @@ -1,44 +1,71 @@ import React from 'react' import { ConnectionType } from 'uiSrc/slices/interfaces' -import { render } from 'uiSrc/utils/test-utils' +import { render, screen } from 'uiSrc/utils/test-utils' import EmptyMessagesList from './EmptyMessagesList' describe('EmptyMessagesList', () => { - it('should render', () => { - expect(render()).toBeTruthy() + it('renders base layout and copy', () => { + render() + + expect(screen.getByTestId('empty-messages-list')).toBeInTheDocument() + + expect(screen.getByText('You are not subscribed')).toBeInTheDocument() + expect( + screen.getByText( + /Subscribe to the Channel to see all the messages published to your database/i, + ), + ).toBeInTheDocument() + + expect( + screen.getByText( + /Running in production may decrease performance and memory available\./i, + ), + ).toBeInTheDocument() }) - it('should render cluster info for Cluster connection type', () => { - const { queryByTestId } = render( + it('shows cluster banner only when Cluster AND isSpublishNotSupported=true', () => { + // visible when both conditions true + const { rerender } = render( , ) + const banner = screen.getByTestId('empty-messages-list-cluster') + expect(banner).toBeInTheDocument() + expect( + screen.getByText( + /Messages published with SPUBLISH will not appear in this channel/i, + ), + ).toBeInTheDocument() - expect(queryByTestId('empty-messages-list-cluster')).toBeInTheDocument() - }) - - it(' not render cluster info for Cluster connection type', () => { - const { queryByTestId } = render( + // hide when flag is false + rerender( , ) + expect( + screen.queryByTestId('empty-messages-list-cluster'), + ).not.toBeInTheDocument() - expect(queryByTestId('empty-messages-list-cluster')).not.toBeInTheDocument() - }) - - it('should not render cluster info for Cluster connection type', () => { - const { queryByTestId } = render( + // hide when connection is not Cluster + rerender( , ) + expect( + screen.queryByTestId('empty-messages-list-cluster'), + ).not.toBeInTheDocument() - expect(queryByTestId('empty-messages-list-cluster')).not.toBeInTheDocument() + // also hide when connectionType is undefined + rerender() + expect( + screen.queryByTestId('empty-messages-list-cluster'), + ).not.toBeInTheDocument() }) }) diff --git a/redisinsight/ui/src/pages/pub-sub/components/messages-list/EmptyMessagesList/EmptyMessagesList.styles.tsx b/redisinsight/ui/src/pages/pub-sub/components/messages-list/EmptyMessagesList/EmptyMessagesList.styles.tsx new file mode 100644 index 0000000000..0de76fe8a7 --- /dev/null +++ b/redisinsight/ui/src/pages/pub-sub/components/messages-list/EmptyMessagesList/EmptyMessagesList.styles.tsx @@ -0,0 +1,22 @@ +import styled from 'styled-components' +import { RiImage } from 'uiSrc/components/base/display' +import { Col, FlexItem } from 'uiSrc/components/base/layout/flex' + +export const HeroImage = styled(RiImage)` + user-select: none; + pointer-events: none; +` + +export const InnerContainer = styled(Col)` + background-color: ${({ theme }) => + theme.semantic.color.background.neutral300}; + border-radius: ${({ theme }) => theme.core.space.space100}; + border: 1px solid ${({ theme }) => theme.semantic.color.border.neutral500}; + padding: ${({ theme }) => theme.core.space.space300}; + height: 100%; +` + +export const Wrapper = styled(FlexItem)` + margin: ${({ theme }) => theme.core.space.space500}; + height: 100%; +` diff --git a/redisinsight/ui/src/pages/pub-sub/components/messages-list/EmptyMessagesList/EmptyMessagesList.tsx b/redisinsight/ui/src/pages/pub-sub/components/messages-list/EmptyMessagesList/EmptyMessagesList.tsx index e164e9c1d2..8b9433760d 100644 --- a/redisinsight/ui/src/pages/pub-sub/components/messages-list/EmptyMessagesList/EmptyMessagesList.tsx +++ b/redisinsight/ui/src/pages/pub-sub/components/messages-list/EmptyMessagesList/EmptyMessagesList.tsx @@ -1,12 +1,13 @@ import React from 'react' -import cx from 'classnames' - import { ConnectionType } from 'uiSrc/slices/interfaces' -import { Text } from 'uiSrc/components/base/text' +import { Text, Title } from 'uiSrc/components/base/text' +import { Col } from 'uiSrc/components/base/layout/flex' +import { Banner } from 'uiSrc/components/base/display' +import { CallOut } from 'uiSrc/components/base/display/call-out/CallOut' +import LightBulbImage from 'uiSrc/assets/img/pub-sub/light-bulb.svg' -import { RiIcon } from 'uiSrc/components/base/icons/RiIcon' -import styles from './styles.module.scss' -import { Row } from 'uiSrc/components/base/layout/flex' +import SubscribeForm from '../../subscribe-form' +import { HeroImage, InnerContainer, Wrapper } from './EmptyMessagesList.styles' export interface Props { connectionType?: ConnectionType @@ -17,38 +18,42 @@ const EmptyMessagesList = ({ connectionType, isSpublishNotSupported, }: Props) => ( -
-
+ - No messages to display - - Subscribe to the Channel to see all the messages published to your - database - - - - - Running in production may decrease performance and memory available + + + + You are not subscribed + + + Subscribe to the Channel to see all the messages published to your + database - + + + + + + Running in production may decrease performance and memory available. + + {connectionType === ConnectionType.Cluster && isSpublishNotSupported && ( <> -
- - {'Messages published with '} - SPUBLISH - {' will not appear in this channel'} - + variant="attention" + showIcon={true} + message="Messages published with SPUBLISH will not appear in this channel" + /> )} -
-
+ + ) export default EmptyMessagesList diff --git a/redisinsight/ui/src/pages/pub-sub/components/messages-list/EmptyMessagesList/styles.module.scss b/redisinsight/ui/src/pages/pub-sub/components/messages-list/EmptyMessagesList/styles.module.scss deleted file mode 100644 index ee6362dc5d..0000000000 --- a/redisinsight/ui/src/pages/pub-sub/components/messages-list/EmptyMessagesList/styles.module.scss +++ /dev/null @@ -1,83 +0,0 @@ -.container { - @include eui.scrollBar; - display: flex; - justify-content: center; - overflow: auto; - - width: 100%; - height: 100%; -} - -.content { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - margin: auto; - padding: 0 20px; - - width: auto; - height: 110px; - - &Cluster { - height: 184px; - } - - .title { - font-size: 16px !important; - line-height: 24px !important; - letter-spacing: 0px !important; - padding-bottom: 24px; - } - - .summary { - font-size: 13px !important; - line-height: 18px !important; - letter-spacing: -0.13px !important; - color: var(--euiColorMediumShade) !important; - padding-bottom: 18px; - } - - .alert { - font-size: 13px !important; - line-height: 18px !important; - letter-spacing: -0.13px !important; - color: var(--euiColorWarningLight) !important; - } - - .cluster { - font-size: 13px !important; - line-height: 18px !important; - letter-spacing: -0.13px !important; - color: var(--textColorShade) !important; - } - - .alertIcon { - margin-right: 6px; - margin-top: -3px; - } - - .badge { - font-size: 12px; - font-weight: 500; - line-height: 18px; - padding: 0 8px; - display: inline-block; - text-decoration: none; - border-radius: 4px; - white-space: nowrap; - vertical-align: middle; - cursor: default; - max-width: 100%; - text-align: left; - color: var(--htmlColor); - background-color: var(--separatorColor); - } - - .separator { - height: 0px; - width: 192px; - border: 1px solid var(--separatorColor); - margin: 30px 0; - } -} diff --git a/redisinsight/ui/src/pages/pub-sub/components/messages-list/MessageListWrapper.styles.tsx b/redisinsight/ui/src/pages/pub-sub/components/messages-list/MessageListWrapper.styles.tsx new file mode 100644 index 0000000000..dfa1aaab02 --- /dev/null +++ b/redisinsight/ui/src/pages/pub-sub/components/messages-list/MessageListWrapper.styles.tsx @@ -0,0 +1,20 @@ +import styled from 'styled-components' +import { Col } from 'uiSrc/components/base/layout/flex' + +export const InnerContainer = styled(Col)` + background-color: ${({ theme }) => + theme.semantic.color.background.neutral300}; + border-radius: ${({ theme }) => theme.core.space.space100}; + border: 1px solid ${({ theme }) => theme.semantic.color.border.neutral500}; + padding: ${({ theme }) => theme.core.space.space300}; + margin: ${({ theme }) => theme.core.space.space200}; +` + +export const Wrapper = styled(Col)` + margin: ${({ theme }) => theme.core.space.space200}; + /* + TODO: Remove margin-top when + don't apply custom padding to the page + */ + margin-top: ${({ theme }) => theme.core.space.space100}; +` diff --git a/redisinsight/ui/src/pages/pub-sub/components/messages-list/MessagesListWrapper.spec.tsx b/redisinsight/ui/src/pages/pub-sub/components/messages-list/MessagesListWrapper.spec.tsx index e2109e1688..17033a679b 100644 --- a/redisinsight/ui/src/pages/pub-sub/components/messages-list/MessagesListWrapper.spec.tsx +++ b/redisinsight/ui/src/pages/pub-sub/components/messages-list/MessagesListWrapper.spec.tsx @@ -1,7 +1,7 @@ import React from 'react' -import { render } from 'uiSrc/utils/test-utils' +import { render, screen } from 'uiSrc/utils/test-utils' -import { pubSubSelector } from 'uiSrc/slices/pubsub/pubsub' +import { pubSubSelector as pubSubSelectorMock } from 'uiSrc/slices/pubsub/pubsub' import MessagesListWrapper from './MessagesListWrapper' jest.mock('uiSrc/slices/pubsub/pubsub', () => ({ @@ -12,11 +12,13 @@ jest.mock('uiSrc/slices/pubsub/pubsub', () => ({ }), })) -describe('MessagesListWrapper', () => { - it('should render', () => { - expect(render()).toBeTruthy() - }) +const pubSubSelector = pubSubSelectorMock as jest.Mock + +afterEach(() => { + jest.clearAllMocks() +}) +describe('MessagesListWrapper', () => { it('should render EmptyMessagesList by default', () => { const { queryByTestId } = render() @@ -24,25 +26,72 @@ describe('MessagesListWrapper', () => { expect(queryByTestId('empty-messages-list')).toBeInTheDocument() }) - it('should render MessagesList if isSubscribed === true', () => { - ;(pubSubSelector as jest.Mock).mockReturnValue({ + it('should render empty MessagesList if client is subscribed and no messages', () => { + pubSubSelector.mockReturnValue({ isSubscribed: true, + messages: [], }) const { queryByTestId } = render() expect(queryByTestId('messages-list')).toBeInTheDocument() expect(queryByTestId('empty-messages-list')).not.toBeInTheDocument() + expect(screen.queryByText('No messages published yet')).toBeInTheDocument() }) - it('should render MessagesList if messages.length !== 0', () => { - ;(pubSubSelector as jest.Mock).mockReturnValue({ + it('should render messages if there are some', () => { + pubSubSelector.mockReturnValue({ messages: [{ time: 123, channel: 'channel', message: 'msg' }], }) - const { queryByTestId } = render() + render() - expect(queryByTestId('messages-list')).toBeInTheDocument() - expect(queryByTestId('empty-messages-list')).not.toBeInTheDocument() + expect(screen.queryByTestId('messages-list')).toBeInTheDocument() + expect(screen.queryByTestId('empty-messages-list')).not.toBeInTheDocument() + expect(screen.queryByText('msg')).toBeInTheDocument() + }) + + it('should render messages if there are some no matter if client is subscribed', () => { + pubSubSelector.mockReturnValue({ + messages: [{ time: 123, channel: 'channel', message: 'msg' }], + isSubscribed: false, + }) + + render() + expect(screen.queryByText('msg')).toBeInTheDocument() + }) + + it('should render header with count and "Subscribed" badge when subscribed and no messages', () => { + pubSubSelector.mockReturnValue({ isSubscribed: true, messages: [] }) + + render() + + expect(screen.getByText('Messages:')).toBeInTheDocument() + expect(screen.getByText('0')).toBeInTheDocument() + expect(screen.getByText('Status:')).toBeInTheDocument() + expect(screen.getByText('Subscribed')).toBeInTheDocument() + + expect(screen.getByText('Timestamp')).toBeInTheDocument() + expect(screen.getByText('Channel')).toBeInTheDocument() + expect(screen.getByText('Message')).toBeInTheDocument() + + expect(screen.getByText('No messages published yet')).toBeInTheDocument() + }) + + it('should render header with count and "Unsubscribed" badge when messages exist but not subscribed', () => { + const items = [ + { time: 123, channel: 'a', message: 'x' }, + { time: 456, channel: 'b', message: 'y' }, + ] + pubSubSelector.mockReturnValue({ isSubscribed: false, messages: items }) + + render() + + expect(screen.getByText('Messages:')).toBeInTheDocument() + expect(screen.getByText(String(items.length))).toBeInTheDocument() + expect(screen.getByText('Status:')).toBeInTheDocument() + expect(screen.getByText('Unsubscribed')).toBeInTheDocument() + + expect(screen.getByTestId('messages-list')).toBeInTheDocument() }) }) diff --git a/redisinsight/ui/src/pages/pub-sub/components/messages-list/MessagesListWrapper.tsx b/redisinsight/ui/src/pages/pub-sub/components/messages-list/MessagesListWrapper.tsx index 451b696e88..f42cb4f5b0 100644 --- a/redisinsight/ui/src/pages/pub-sub/components/messages-list/MessagesListWrapper.tsx +++ b/redisinsight/ui/src/pages/pub-sub/components/messages-list/MessagesListWrapper.tsx @@ -7,16 +7,31 @@ import { pubSubSelector } from 'uiSrc/slices/pubsub/pubsub' import { isVersionHigherOrEquals } from 'uiSrc/utils' import { CommandsVersions } from 'uiSrc/constants/commandsVersions' import { useConnectionType } from 'uiSrc/components/hooks/useConnectionType' +import { DEFAULT_SEARCH_MATCH } from 'uiSrc/constants/api' import EmptyMessagesList from './EmptyMessagesList' import MessagesList from './MessagesList' -import styles from './MessagesList/styles.module.scss' +import { Row } from 'uiSrc/components/base/layout/flex' +import { Text } from 'uiSrc/components/base/text' +import { RiBadge } from 'uiSrc/components/base/display/badge/RiBadge' +import { HorizontalSpacer } from 'uiSrc/components/base/layout' +import SubscribeForm from '../subscribe-form' +import PatternsInfo from '../patternsInfo' +import { InnerContainer, Wrapper } from './MessageListWrapper.styles' const MessagesListWrapper = () => { - const { messages = [], isSubscribed } = useSelector(pubSubSelector) + const { + messages = [], + isSubscribed, + subscriptions, + } = useSelector(pubSubSelector) const connectionType = useConnectionType() const { version } = useSelector(connectedInstanceOverviewSelector) + const channels = subscriptions?.length + ? subscriptions.map((sub) => sub.channel).join(' ') + : DEFAULT_SEARCH_MATCH + const [isSpublishNotSupported, setIsSpublishNotSupported] = useState(true) @@ -29,31 +44,66 @@ const MessagesListWrapper = () => { ) }, [version]) - return ( - <> - {(messages.length > 0 || isSubscribed) && ( -
-
-
Timestamp
-
Channel
-
Message
-
-
+ const hasMessages = messages.length > 0 + + if (hasMessages || isSubscribed) { + return ( + + + + + + + Messages: + {messages.length} + + + + + Status: + {isSubscribed ? ( + + ) : ( + + )} + + + + + + + + + Timestamp + + Channel + + Message + + + {hasMessages && ( {({ width, height }) => ( )} -
-
- )} - {messages.length === 0 && !isSubscribed && ( - - )} - + )} + + {!hasMessages && ( + + No messages published yet + + )} + + + ) + } + + return ( + ) } diff --git a/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/components/patternsInfo/PatternsInfo.spec.tsx b/redisinsight/ui/src/pages/pub-sub/components/patternsInfo/PatternsInfo.spec.tsx similarity index 99% rename from redisinsight/ui/src/pages/pub-sub/components/subscription-panel/components/patternsInfo/PatternsInfo.spec.tsx rename to redisinsight/ui/src/pages/pub-sub/components/patternsInfo/PatternsInfo.spec.tsx index dfd817ec34..4778e8f4da 100644 --- a/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/components/patternsInfo/PatternsInfo.spec.tsx +++ b/redisinsight/ui/src/pages/pub-sub/components/patternsInfo/PatternsInfo.spec.tsx @@ -11,6 +11,7 @@ describe('PatternsInfo', () => { const content = 'hello' render() expect(screen.getByText('Patterns: 1')).toBeInTheDocument() + fireEvent.focus(screen.getByTestId('append-info-icon')) await waitFor(() => screen.getAllByText(content)) expect(screen.getAllByText(content)[0]).toBeInTheDocument() diff --git a/redisinsight/ui/src/pages/pub-sub/components/patternsInfo/PatternsInfo.styles.tsx b/redisinsight/ui/src/pages/pub-sub/components/patternsInfo/PatternsInfo.styles.tsx new file mode 100644 index 0000000000..c7195544f5 --- /dev/null +++ b/redisinsight/ui/src/pages/pub-sub/components/patternsInfo/PatternsInfo.styles.tsx @@ -0,0 +1,12 @@ +import styled from 'styled-components' +import { RiIcon } from 'uiSrc/components/base/icons' + +export const InfoIcon = styled(RiIcon).attrs({ + type: 'InfoIcon', + 'data-testid': 'append-info-icon', +})` + cursor: pointer; + // TODO: Remove margin-top + // Hack: for some reason this icon has extra height, which breaks flex alignment + margin-top: 4px; +` diff --git a/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/components/patternsInfo/PatternsInfo.tsx b/redisinsight/ui/src/pages/pub-sub/components/patternsInfo/PatternsInfo.tsx similarity index 58% rename from redisinsight/ui/src/pages/pub-sub/components/subscription-panel/components/patternsInfo/PatternsInfo.tsx rename to redisinsight/ui/src/pages/pub-sub/components/patternsInfo/PatternsInfo.tsx index b9ac92becd..dc638099e5 100644 --- a/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/components/patternsInfo/PatternsInfo.tsx +++ b/redisinsight/ui/src/pages/pub-sub/components/patternsInfo/PatternsInfo.tsx @@ -1,10 +1,11 @@ import React from 'react' -import { DEFAULT_SEARCH_MATCH } from 'uiSrc/constants/api' -import { Text } from 'uiSrc/components/base/text' import { RiTooltip } from 'uiSrc/components' +import { DEFAULT_SEARCH_MATCH } from 'uiSrc/constants/api' -import { RiIcon } from 'uiSrc/components/base/icons/RiIcon' -import styles from './styles.module.scss' +import { Text } from 'uiSrc/components/base/text' +import { Row } from 'uiSrc/components/base/layout/flex' +import { HorizontalSpacer } from 'uiSrc/components/base/layout' +import { InfoIcon } from './PatternsInfo.styles' export interface PatternsInfoProps { channels?: string @@ -17,29 +18,27 @@ const PatternsInfo = ({ channels }: PatternsInfoProps) => { } return ( -
- - Patterns: {getChannelsCount()}{' '} + + + Patterns: {getChannelsCount()} + + + {channels ?.trim() .split(' ') - .map((ch) =>

{ch}

)} + .map((ch) => {ch})} } > - +
-
+ ) } diff --git a/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/components/patternsInfo/index.ts b/redisinsight/ui/src/pages/pub-sub/components/patternsInfo/index.ts similarity index 100% rename from redisinsight/ui/src/pages/pub-sub/components/subscription-panel/components/patternsInfo/index.ts rename to redisinsight/ui/src/pages/pub-sub/components/patternsInfo/index.ts diff --git a/redisinsight/ui/src/pages/pub-sub/components/publish-message/PublishMessage.spec.tsx b/redisinsight/ui/src/pages/pub-sub/components/publish-message/PublishMessage.spec.tsx index 7e7f13021b..dedf6610e3 100644 --- a/redisinsight/ui/src/pages/pub-sub/components/publish-message/PublishMessage.spec.tsx +++ b/redisinsight/ui/src/pages/pub-sub/components/publish-message/PublishMessage.spec.tsx @@ -1,37 +1,238 @@ -import { fireEvent } from '@testing-library/react' -import { cloneDeep } from 'lodash' import React from 'react' -import { publishMessage } from 'uiSrc/slices/pubsub/pubsub' +import { act, fireEvent, waitFor } from '@testing-library/react' +import { cloneDeep } from 'lodash' + import { cleanup, - clearStoreActions, + initialStateDefault, + mockStore, mockedStore, render, screen, } from 'uiSrc/utils/test-utils' +import { ConnectionType } from 'uiSrc/slices/interfaces' import PublishMessage from './PublishMessage' +import { publishMessageAction } from 'uiSrc/slices/pubsub/pubsub' +import { setPubSubFieldsContext } from 'uiSrc/slices/app/context' + +let mockedConnType = ConnectionType.Standalone +jest.mock('uiSrc/components/hooks/useConnectionType', () => ({ + useConnectionType: () => mockedConnType, +})) + +jest.mock('uiSrc/slices/pubsub/pubsub', () => { + const actual = jest.requireActual('uiSrc/slices/pubsub/pubsub') + return { + ...actual, + publishMessageAction: jest.fn( + (instanceId, channel, message, onSuccess) => (dispatch: any) => { + const action = { + type: 'pubsub/publishMessageAction', + payload: [instanceId, channel, message, onSuccess], + } + dispatch(action) + return Promise.resolve() + }, + ), + } +}) + +jest.mock('uiSrc/slices/app/context', () => { + const actual = jest.requireActual('uiSrc/slices/app/context') + return { + ...actual, + appContextPubSub: (state: any) => + state?.app?.context?.pubsub ?? { channel: '', message: '' }, + setPubSubFieldsContext: jest.fn((fields: any) => (dispatch: any) => { + const action = { + type: 'app/setPubSubFieldsContext', + payload: fields, + } + dispatch(action) + return action + }), + } +}) let store: typeof mockedStore +const createTestStateWithContext = ( + contextOverrides = {}, + pubsubOverrides = {}, +) => { + const state = cloneDeep(initialStateDefault) + state.app.context = { + ...state.app.context, + ...contextOverrides, + } + state.pubsub = { + loading: false, + publishing: false, + error: '', + subscriptions: [], + isSubscribeTriggered: false, + isConnected: false, + isSubscribed: false, + messages: [], + count: 0, + ...pubsubOverrides, + } + return state +} + +const renderPublishMessage = (contextOverrides = {}, pubsubOverrides = {}) => { + const initialStoreState = createTestStateWithContext( + contextOverrides, + pubsubOverrides, + ) + return render(, { + store: mockStore(initialStoreState), + }) +} + +const getChannelField = () => screen.getByTestId('field-channel-name') +const getMessageField = () => screen.getByTestId('field-message') +const getSubmitBtn = () => screen.getByTestId('publish-message-submit') + beforeEach(() => { + jest.useFakeTimers() cleanup() store = cloneDeep(mockedStore) store.clearActions() + jest.mocked(publishMessageAction).mockClear() + jest.mocked(setPubSubFieldsContext).mockClear() + mockedConnType = ConnectionType.Standalone +}) + +afterEach(() => { + jest.runOnlyPendingTimers() + jest.useRealTimers() }) describe('PublishMessage', () => { - it('should render', () => { + it('should render basic form fields and button', () => { expect(render()).toBeTruthy() + expect(getChannelField()).toBeInTheDocument() + expect(getMessageField()).toBeInTheDocument() + expect(getSubmitBtn()).toBeInTheDocument() + }) + + it('should initialize channel/message from app context', async () => { + renderPublishMessage({ + pubsub: { channel: 'orders', message: 'hello' }, + }) + + await waitFor(() => { + expect(screen.getByDisplayValue('orders')).toBeInTheDocument() + expect(screen.getByDisplayValue('hello')).toBeInTheDocument() + }) }) - it('should dispatch subscribe action after submit', () => { + it('should dispatche publish action with instanceId, channel, message, and a callback', () => { + render(, { + store: mockedStore, + }) + + fireEvent.change(getChannelField(), { + target: { value: 'news' }, + }) + fireEvent.change(getMessageField(), { + target: { value: 'ping' }, + }) + + fireEvent.click(getSubmitBtn()) + + const actions = mockedStore.getActions() + expect(actions[0].type).toBe('pubsub/publishMessageAction') + + const [iid, ch, msg, cb] = actions[0].payload + expect(iid).toBe('instanceId') + expect(ch).toBe('news') + expect(msg).toBe('ping') + expect(typeof cb).toBe('function') + }) + + it('should clear message, shows success badge with affected clients, hides button on success published message', async () => { render() - const expectedActions = [publishMessage()] - fireEvent.click(screen.getByTestId('publish-message-submit')) - expect(clearStoreActions(store.getActions())).toEqual( - clearStoreActions(expectedActions), + fireEvent.change(getChannelField(), { + target: { value: 'alpha' }, + }) + fireEvent.change(getMessageField(), { + target: { value: 'hello world' }, + }) + fireEvent.click(getSubmitBtn()) + + const [, , , onSuccess] = mockedStore.getActions()[0].payload + const affectedClients = 7 + act(() => onSuccess(affectedClients)) + + await waitFor(() => { + expect(getMessageField()).toHaveValue('') + expect( + screen.queryByTestId('publish-message-submit'), + ).not.toBeInTheDocument() + }) + + expect( + screen.getByText(`Published (${affectedClients})`), + ).toBeInTheDocument() + }) + + it('should hide success badge client count (just "Published") when connection type is cluster', async () => { + mockedConnType = ConnectionType.Cluster + render() + + fireEvent.click(getSubmitBtn()) + const [, , , onSuccess] = mockedStore.getActions()[0].payload + const affectedClients = 123 + act(() => onSuccess(affectedClients)) + + await waitFor(() => { + expect(screen.getByText(/^Published$/)).toBeInTheDocument() + }) + expect( + screen.queryByText(`Published (${affectedClients})`), + ).not.toBeInTheDocument() + }) + + it('should auto-hide success badge after HIDE_BADGE_TIMER and shows submit button again', () => { + render() + + fireEvent.click(getSubmitBtn()) + const [, , , onSuccess] = mockedStore.getActions()[0].payload + act(() => onSuccess(1)) + + expect(screen.getByText(/Published/)).toBeInTheDocument() + + act(() => { + jest.advanceTimersByTime(3000) + }) + + expect(screen.queryByText(/Published/)).not.toBeInTheDocument() + expect(getSubmitBtn()).toBeInTheDocument() + }) + + it('should persist latest channel/message to context on unmount', () => { + const { unmount } = render(, { + store: mockedStore, + }) + + fireEvent.change(getChannelField(), { + target: { value: 'finalCh' }, + }) + fireEvent.change(getMessageField(), { + target: { value: 'finalMsg' }, + }) + + unmount() + + const actions = mockedStore.getActions() + const setCtx = actions.find( + (a: any) => a.type === 'app/setPubSubFieldsContext', ) + expect(setCtx).toBeTruthy() + expect(setCtx.payload).toEqual({ channel: 'finalCh', message: 'finalMsg' }) }) }) diff --git a/redisinsight/ui/src/pages/pub-sub/components/publish-message/PublishMessage.styles.tsx b/redisinsight/ui/src/pages/pub-sub/components/publish-message/PublishMessage.styles.tsx new file mode 100644 index 0000000000..91ccbcb46e --- /dev/null +++ b/redisinsight/ui/src/pages/pub-sub/components/publish-message/PublishMessage.styles.tsx @@ -0,0 +1,16 @@ +import styled from 'styled-components' +import { Col, Row } from 'uiSrc/components/base/layout/flex' + +export const ChannelColumn = styled(Col)` + // There are 2 columns next to each other. + // The channel one doesn't grow, but it should have a minimum width. + min-width: 250px; +` + +export const ButtonWrapper = styled(Row)` + min-width: 100px; +` + +export const ResultWrapper = styled(Row)` + min-height: 36px; +` diff --git a/redisinsight/ui/src/pages/pub-sub/components/publish-message/PublishMessage.tsx b/redisinsight/ui/src/pages/pub-sub/components/publish-message/PublishMessage.tsx index 37738f4738..ac249647bf 100644 --- a/redisinsight/ui/src/pages/pub-sub/components/publish-message/PublishMessage.tsx +++ b/redisinsight/ui/src/pages/pub-sub/components/publish-message/PublishMessage.tsx @@ -1,10 +1,4 @@ -import cx from 'classnames' -import React, { - FormEvent, - useEffect, - useRef, - useState, -} from 'react' +import React, { FormEvent, useEffect, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { useParams } from 'react-router-dom' import { @@ -15,14 +9,17 @@ import { ConnectionType } from 'uiSrc/slices/interfaces' import { publishMessageAction } from 'uiSrc/slices/pubsub/pubsub' import { useConnectionType } from 'uiSrc/components/hooks/useConnectionType' -import { FlexItem, Row } from 'uiSrc/components/base/layout/flex' +import { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex' import { PrimaryButton } from 'uiSrc/components/base/forms/buttons' import { FormField } from 'uiSrc/components/base/forms/FormField' -import { RiBadge } from 'uiSrc/components/base/display/badge/RiBadge' -import { CheckThinIcon } from 'uiSrc/components/base/icons' -import { RiIcon } from 'uiSrc/components/base/icons/RiIcon' +import { ToastCheckIcon, Icon } from 'uiSrc/components/base/icons' import { TextInput } from 'uiSrc/components/base/inputs' -import styles from './styles.module.scss' +import { Text } from 'uiSrc/components/base/text' +import { + ButtonWrapper, + ChannelColumn, + ResultWrapper, +} from './PublishMessage.styles' const HIDE_BADGE_TIMER = 3000 @@ -78,74 +75,68 @@ const PublishMessage = () => { dispatch(publishMessageAction(instanceId, channel, message, onSuccess)) } + const getClientsText = (clients?: number) => + typeof clients !== 'number' ? 'Published' : `Published (${clients})` + return ( -
- - - + + + + + Channel name - setChannel(value) - } + onChange={(value) => setChannel(value)} autoComplete="off" data-testid="field-channel-name" /> - - - - <> - - setMessage(value) - } - autoComplete="off" - data-testid="field-message" - /> - - {connectionType !== ConnectionType.Cluster && ( - - - {affectedClients} - - - - )} - - - - + + + + Message + setMessage(value)} + autoComplete="off" + data-testid="field-message" + /> + - - - - - Publish - - + + {isShowBadge && ( + + + + {getClientsText( + connectionType !== ConnectionType.Cluster + ? affectedClients + : undefined, + )} + + + )} + + {!isShowBadge && ( + + + + Publish + + + + )}
) diff --git a/redisinsight/ui/src/pages/pub-sub/components/publish-message/styles.module.scss b/redisinsight/ui/src/pages/pub-sub/components/publish-message/styles.module.scss deleted file mode 100644 index f646330b08..0000000000 --- a/redisinsight/ui/src/pages/pub-sub/components/publish-message/styles.module.scss +++ /dev/null @@ -1,46 +0,0 @@ -.container { - .channelWrapper { - min-width: 180px; - } - .messageWrapper { - flex-grow: 3 !important; - position: relative; - - .messageField { - &.showBadge { - padding-right: 80px; - } - } - } - - .badge { - position: absolute; - background-color: var(--pubSubClientsBadge) !important; - top: 50%; - right: 8px; - transform: translateY(-50%); - color: var(--htmlColor) !important; - opacity: 0; - pointer-events: none; - transition: opacity 250ms ease-in-out; - - &.show { - opacity: 1; - pointer-events: auto; - } - - :global(.euiBadge__text) { - display: flex; - align-items: center; - } - - .affectedClients { - margin-left: 6px; - } - - .iconUserBadge { - color: var(--htmlColor) !important; - margin-bottom: 2px; - } - } -} diff --git a/redisinsight/ui/src/pages/pub-sub/components/subscribe-form/SubscribeForm.spec.tsx b/redisinsight/ui/src/pages/pub-sub/components/subscribe-form/SubscribeForm.spec.tsx new file mode 100644 index 0000000000..d37a75d205 --- /dev/null +++ b/redisinsight/ui/src/pages/pub-sub/components/subscribe-form/SubscribeForm.spec.tsx @@ -0,0 +1,134 @@ +// SubscribeForm.spec.tsx +import React from 'react' +import { fireEvent, waitFor } from '@testing-library/react' +import { cloneDeep } from 'lodash' +import { toggleSubscribeTriggerPubSub } from 'uiSrc/slices/pubsub/pubsub' +import { + cleanup, + clearStoreActions, + initialStateDefault, + mockStore, + mockedStore, + render, + screen, +} from 'uiSrc/utils/test-utils' + +import SubscribeForm from './SubscribeForm' +import { SubscriptionType } from 'apiSrc/modules/pub-sub/constants' + +let store: typeof mockedStore + +const createTestStateWithPubSub = (pubsubOverrides = {}) => { + const state = cloneDeep(initialStateDefault) + state.pubsub = { + isSubscribed: false, + loading: false, + publishing: false, + error: '', + isSubscribeTriggered: false, + isConnected: false, + subscriptions: [], + messages: [], + count: 0, + ...pubsubOverrides, + } + return state +} + +const renderSubscribeForm = (pubsubOverrides = {}) => { + const initialStoreState = createTestStateWithPubSub(pubsubOverrides) + return render(, { + store: mockStore(initialStoreState), + }) +} + +const getChannelsInput = () => screen.getByTestId('channels-input') +const getSubscribeBtn = () => screen.getByTestId('subscribe-btn') + +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +describe('SubscribeForm', () => { + it('should initialize channels from subscriptions', async () => { + renderSubscribeForm({ + subscriptions: [ + { channel: 'a.*', type: SubscriptionType.PSubscribe }, + { channel: 'b.c', type: SubscriptionType.PSubscribe }, + ], + }) + + await waitFor(() => + expect(screen.getByDisplayValue('a.* b.c')).toBeInTheDocument(), + ) + }) + + it('should use default "*" when no subscriptions', async () => { + renderSubscribeForm({ + subscriptions: [], + }) + + await waitFor(() => + expect(screen.getByDisplayValue('*')).toBeInTheDocument(), + ) + }) + + it('should restore default "*" on blur when empty', async () => { + render() + + fireEvent.change(getChannelsInput(), { + target: { value: '' }, + }) + fireEvent.blur(getChannelsInput()) + + await waitFor(() => + expect(screen.getByDisplayValue('*')).toBeInTheDocument(), + ) + }) + + it('should update channels as user types', () => { + render() + fireEvent.change(getChannelsInput(), { + target: { value: 'alpha beta.*' }, + }) + expect(screen.getByDisplayValue('alpha beta.*')).toBeInTheDocument() + }) + + it('should dispatch toggleSubscribe with current channels value', () => { + render() + + fireEvent.change(getChannelsInput(), { + target: { value: 'news.* logs.error' }, + }) + fireEvent.click(getSubscribeBtn()) + + expect(clearStoreActions(mockedStore.getActions())).toEqual( + clearStoreActions([toggleSubscribeTriggerPubSub('news.* logs.error')]), + ) + }) + + it('should disable input when subscribed and shows "Unsubscribe" label', () => { + renderSubscribeForm({ + isSubscribed: true, + }) + + expect(getChannelsInput()).toBeDisabled() + expect(getSubscribeBtn()).toHaveTextContent('Unsubscribe') + }) + + it('should show "Subscribe" label when not subscribed', () => { + renderSubscribeForm({ + isSubscribed: false, + }) + expect(getSubscribeBtn()).toHaveTextContent('Subscribe') + }) + + it('should disable subscribe button when loading', () => { + renderSubscribeForm({ + loading: true, + }) + expect(getSubscribeBtn()).toBeDisabled() + }) +}) diff --git a/redisinsight/ui/src/pages/pub-sub/components/subscribe-form/SubscribeForm.styles.tsx b/redisinsight/ui/src/pages/pub-sub/components/subscribe-form/SubscribeForm.styles.tsx new file mode 100644 index 0000000000..23c4329bdb --- /dev/null +++ b/redisinsight/ui/src/pages/pub-sub/components/subscribe-form/SubscribeForm.styles.tsx @@ -0,0 +1,6 @@ +import styled from 'styled-components' +import { TextInput } from 'uiSrc/components/base/inputs' + +export const TopicNameField = styled(TextInput)` + min-width: 250px; +` diff --git a/redisinsight/ui/src/pages/pub-sub/components/subscribe-form/SubscribeForm.tsx b/redisinsight/ui/src/pages/pub-sub/components/subscribe-form/SubscribeForm.tsx new file mode 100644 index 0000000000..3a5b5e4997 --- /dev/null +++ b/redisinsight/ui/src/pages/pub-sub/components/subscribe-form/SubscribeForm.tsx @@ -0,0 +1,71 @@ +import React, { useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { + pubSubSelector, + toggleSubscribeTriggerPubSub, +} from 'uiSrc/slices/pubsub/pubsub' + +import { Button } from 'uiSrc/components/base/forms/buttons' +import { FormField } from 'uiSrc/components/base/forms/FormField' +import { Row } from 'uiSrc/components/base/layout/flex' +import { DEFAULT_SEARCH_MATCH } from 'uiSrc/constants/api' + +import { UserIcon, IndicatorExcludedIcon } from 'uiSrc/components/base/icons' +import { FlexProps } from 'uiSrc/components/base/layout/flex/flex.styles' +import SubscribeInformation from '../subscribe-information' +import { TopicNameField } from './SubscribeForm.styles' + +export interface SubscribeFormProps extends Omit {} + +const SubscribeForm = (props: SubscribeFormProps) => { + const dispatch = useDispatch() + + const { isSubscribed, subscriptions, loading } = useSelector(pubSubSelector) + + const [channels, setChannels] = useState(() => + subscriptions?.length + ? subscriptions.map((sub) => sub.channel).join(' ') + : DEFAULT_SEARCH_MATCH, + ) + + const onFocusOut = () => { + if (!channels) { + setChannels(DEFAULT_SEARCH_MATCH) + } + } + + const toggleSubscribe = () => { + dispatch(toggleSubscribeTriggerPubSub(channels)) + } + + return ( + + + setChannels(value)} + onBlur={onFocusOut} + placeholder="Enter Pattern" + aria-label="channel names for filtering" + data-testid="channels-input" + /> + + + + + + + ) +} + +export default SubscribeForm diff --git a/redisinsight/ui/src/pages/pub-sub/components/subscribe-form/index.ts b/redisinsight/ui/src/pages/pub-sub/components/subscribe-form/index.ts new file mode 100644 index 0000000000..b6aa3927b0 --- /dev/null +++ b/redisinsight/ui/src/pages/pub-sub/components/subscribe-form/index.ts @@ -0,0 +1,3 @@ +import SubscribeForm from './SubscribeForm' + +export default SubscribeForm diff --git a/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/components/clickable-append-info/ClickableAppendInfo.spec.tsx b/redisinsight/ui/src/pages/pub-sub/components/subscribe-information/SubscribeInformation.spec.tsx similarity index 62% rename from redisinsight/ui/src/pages/pub-sub/components/subscription-panel/components/clickable-append-info/ClickableAppendInfo.spec.tsx rename to redisinsight/ui/src/pages/pub-sub/components/subscribe-information/SubscribeInformation.spec.tsx index 93c54226eb..09795d4975 100644 --- a/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/components/clickable-append-info/ClickableAppendInfo.spec.tsx +++ b/redisinsight/ui/src/pages/pub-sub/components/subscribe-information/SubscribeInformation.spec.tsx @@ -1,14 +1,15 @@ import React from 'react' import { fireEvent, render, screen } from 'uiSrc/utils/test-utils' -import ClickableAppendInfo from './ClickableAppendInfo' +import SubscribeInformation from './SubscribeInformation' -describe('ClickableAppendInfo', () => { +describe('SubscribeInformation', () => { it('should render', () => { - expect(render()).toBeTruthy() + expect(render()).toBeTruthy() }) it('should open popover on click', async () => { - render() + render() + fireEvent.click(screen.getByTestId('append-info-icon')) expect(screen.getByTestId('pub-sub-examples')).toBeInTheDocument() }) diff --git a/redisinsight/ui/src/pages/pub-sub/components/subscribe-information/SubscribeInformation.styles.tsx b/redisinsight/ui/src/pages/pub-sub/components/subscribe-information/SubscribeInformation.styles.tsx new file mode 100644 index 0000000000..63dde5a5f7 --- /dev/null +++ b/redisinsight/ui/src/pages/pub-sub/components/subscribe-information/SubscribeInformation.styles.tsx @@ -0,0 +1,13 @@ +import styled from 'styled-components' +import { RiIcon } from 'uiSrc/components/base/icons' + +export const InfoIcon = styled(RiIcon).attrs({ + type: 'InfoIcon', + size: 'l', + 'data-testid': 'append-info-icon', +})` + cursor: pointer; + // TODO: Remove margin-top + // Hack: for some reason this icon has extra height, which breaks flex alignment + margin-top: 4px; +` diff --git a/redisinsight/ui/src/pages/pub-sub/components/subscribe-information/SubscribeInformation.tsx b/redisinsight/ui/src/pages/pub-sub/components/subscribe-information/SubscribeInformation.tsx new file mode 100644 index 0000000000..4c54836bb4 --- /dev/null +++ b/redisinsight/ui/src/pages/pub-sub/components/subscribe-information/SubscribeInformation.tsx @@ -0,0 +1,43 @@ +import React from 'react' +import { getUtmExternalLink } from 'uiSrc/utils/links' +import { Text } from 'uiSrc/components/base/text' +import { + EXTERNAL_LINKS, + UTM_CAMPAINGS, + UTM_MEDIUMS, +} from 'uiSrc/constants/links' +import { Link } from 'uiSrc/components/base/link/Link' +import { RiPopover } from 'uiSrc/components/base' +import { Col } from 'uiSrc/components/base/layout/flex' +import { InfoIcon } from './SubscribeInformation.styles' + +const SubscribeInformation = () => ( + } + data-testid="pub-sub-examples" + > + + + Subscribe to one or more channels or patterns by entering them, + separated by spaces. + + + + Supported glob-style patterns are described  + + here. + + + + +) + +export default SubscribeInformation diff --git a/redisinsight/ui/src/pages/pub-sub/components/subscribe-information/index.ts b/redisinsight/ui/src/pages/pub-sub/components/subscribe-information/index.ts new file mode 100644 index 0000000000..462ec6e39b --- /dev/null +++ b/redisinsight/ui/src/pages/pub-sub/components/subscribe-information/index.ts @@ -0,0 +1,3 @@ +import SubscribeInformation from './SubscribeInformation' + +export default SubscribeInformation 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 deleted file mode 100644 index 0d1079559b..0000000000 --- a/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/SubscriptionPanel.spec.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react' -import { fireEvent, waitFor } 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), - ) - }) - - it('should set default value on blur when empty', async () => { - render() - fireEvent.change(screen.getByTestId('channels-input'), { - target: { value: '' }, - }) - fireEvent.blur(screen.getByTestId('channels-input')) - - await waitFor(() => screen.getByDisplayValue('*')) - expect(screen.getByDisplayValue('*')).toBeInTheDocument() - }) -}) 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 deleted file mode 100644 index 0dab4bfe29..0000000000 --- a/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/SubscriptionPanel.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import cx from 'classnames' -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 { - clearPubSubMessages, - pubSubSelector, - toggleSubscribeTriggerPubSub, -} from 'uiSrc/slices/pubsub/pubsub' -import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' - -import { DEFAULT_SEARCH_MATCH } from 'uiSrc/constants/api' -import { FlexItem, Row } from 'uiSrc/components/base/layout/flex' -import { - UserIcon, - IndicatorExcludedIcon, - DeleteIcon, -} from 'uiSrc/components/base/icons' -import { Button, IconButton } from 'uiSrc/components/base/forms/buttons' -import { Text } from 'uiSrc/components/base/text' -import { RiTooltip } from 'uiSrc/components' -import { AllIconsType, RiIcon } from 'uiSrc/components/base/icons/RiIcon' -import { TextInput } from 'uiSrc/components/base/inputs' -import { FormField } from 'uiSrc/components/base/forms/FormField' -import PatternsInfo from './components/patternsInfo' -import ClickableAppendInfo from './components/clickable-append-info' -import styles from './styles.module.scss' - -const SubscriptionPanel = () => { - const { messages, isSubscribed, subscriptions, loading, count } = - useSelector(pubSubSelector) - - const dispatch = useDispatch() - const { theme } = useContext(ThemeContext) - - const { instanceId = '' } = useParams<{ instanceId: string }>() - - const [channels, setChannels] = useState( - subscriptions?.length - ? subscriptions.map((sub) => sub.channel).join(' ') - : DEFAULT_SEARCH_MATCH, - ) - - const toggleSubscribe = () => { - dispatch(toggleSubscribeTriggerPubSub(channels)) - } - - const onClickClear = () => { - dispatch(clearPubSubMessages()) - sendEventTelemetry({ - event: TelemetryEvent.PUBSUB_MESSAGES_CLEARED, - eventData: { - databaseId: instanceId, - messages: count, - }, - }) - } - - const onFocusOut = () => { - if (!channels) { - setChannels(DEFAULT_SEARCH_MATCH) - } - } - - const subscribedIcon: AllIconsType = - theme === Theme.Dark ? 'SubscribedDarkIcon' : 'SubscribedLightIcon' - const notSubscribedIcon = - theme === Theme.Dark ? 'NotSubscribedDarkIcon' : 'NotSubscribedLightIcon' - - const displayMessages = count !== 0 || isSubscribed - - return ( - - - - - - - - - You are {!isSubscribed && 'not'} subscribed - - - {isSubscribed && ( - - - - )} - {displayMessages && ( - - - Messages: {count} - - - )} - - - - - - - - - - setChannels(value)} - onBlur={onFocusOut} - placeholder="Enter Pattern" - aria-label="channel names for filtering" - data-testid="channels-input" - /> - - - - - - {!!messages.length && ( - - - - - - )} - - - - ) -} - -export default SubscriptionPanel diff --git a/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/components/clickable-append-info/ClickableAppendInfo.tsx b/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/components/clickable-append-info/ClickableAppendInfo.tsx deleted file mode 100644 index a2c0cfb7f4..0000000000 --- a/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/components/clickable-append-info/ClickableAppendInfo.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { useState } from 'react' -import { getUtmExternalLink } from 'uiSrc/utils/links' -import { Text } from 'uiSrc/components/base/text' -import { - EXTERNAL_LINKS, - UTM_CAMPAINGS, - UTM_MEDIUMS, -} from 'uiSrc/constants/links' -import { RiIcon } from 'uiSrc/components/base/icons/RiIcon' -import { Link } from 'uiSrc/components/base/link/Link' -import { RiPopover } from 'uiSrc/components/base' -import styles from './styles.module.scss' - -const ClickableAppendInfo = () => { - const [open, setOpen] = useState(false) - - const onClick = () => { - const newVal = !open - setOpen(newVal) - } - - return ( - - } - isOpen={open} - closePopover={() => setOpen(false)} - panelClassName={styles.popover} - anchorClassName={styles.infoIcon} - panelPaddingSize="s" - data-testid="pub-sub-examples" - > - - Subscribe to one or more channels or patterns by entering them, - separated by spaces. -
- Supported glob-style patterns are described  - - here. - -
-
- ) -} - -export default ClickableAppendInfo diff --git a/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/components/clickable-append-info/index.ts b/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/components/clickable-append-info/index.ts deleted file mode 100644 index 89e433f906..0000000000 --- a/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/components/clickable-append-info/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import ClickableAppendInfo from './ClickableAppendInfo' - -export default ClickableAppendInfo diff --git a/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/components/clickable-append-info/styles.module.scss b/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/components/clickable-append-info/styles.module.scss deleted file mode 100644 index 71d9ebf269..0000000000 --- a/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/components/clickable-append-info/styles.module.scss +++ /dev/null @@ -1,8 +0,0 @@ -.infoIcon { - width: 34px; -} - -.popover { - max-width: 250px !important; - border-radius: 4px; -} diff --git a/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/components/patternsInfo/styles.module.scss b/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/components/patternsInfo/styles.module.scss deleted file mode 100644 index 7b7d2a8dae..0000000000 --- a/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/components/patternsInfo/styles.module.scss +++ /dev/null @@ -1,9 +0,0 @@ -.patternsContainer { - display: flex; - align-items: center; -} - -.appendIcon { - color: var(--iconsDefaultColor) !important; - margin-left: 4px; -} diff --git a/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/index.ts b/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/index.ts deleted file mode 100644 index 675861ad29..0000000000 --- a/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import SubscriptionPanel from './SubscriptionPanel' - -export default SubscriptionPanel 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 deleted file mode 100644 index afa5c66969..0000000000 --- a/redisinsight/ui/src/pages/pub-sub/components/subscription-panel/styles.module.scss +++ /dev/null @@ -1,30 +0,0 @@ -.buttonSubscribe { - :global(.euiButton__text) { - font-weight: normal !important; - font-size: 12px !important; - } -} - -.container { - height: 30px; - margin: 0 -4px !important; -} - -.iconSubscribe { - width: 18px; - height: 18px; - margin-right: 6px; - - .iconUser { - width: 18px; - height: 18px; - } -} - -.channels { - margin-right: 8px; - - :global(.euiFormControlLayout--compressed .inputAppendIcon > svg) { - height: 31px !important; - } -} diff --git a/redisinsight/ui/src/pages/pub-sub/styles.module.scss b/redisinsight/ui/src/pages/pub-sub/styles.module.scss deleted file mode 100644 index 85f8f66443..0000000000 --- a/redisinsight/ui/src/pages/pub-sub/styles.module.scss +++ /dev/null @@ -1,24 +0,0 @@ -.main { - margin: 0 16px 0; - height: 100%; - display: flex; - flex-direction: column; - - .tableWrapper { - width: 100%; - height: calc(100% - 125px); - padding: 18px 0 0 18px; - - :global(.ReactVirtualized__Grid) { - @include eui.scrollBar; - } - } -} - -.onboardAnchor { - align-self: end; -} - -.onboardPanel { - margin-right: 15px; // Dirty placement fix, to position popover in the bottom right corner of the screen, in line with the browser containers -}