Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions redisinsight/ui/src/constants/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ enum ApiEndpoints {

SLOW_LOGS = 'slow-logs',
SLOW_LOGS_CONFIG = 'slow-logs/config',

PUB_SUB = 'pub-sub',
PUB_SUB_MESSAGES = 'pub-sub/messages'
}

export const DEFAULT_SEARCH_MATCH = '*'
Expand Down
4 changes: 2 additions & 2 deletions redisinsight/ui/src/pages/pubSub/PubSubPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react'
import InstanceHeader from 'uiSrc/components/instance-header'
import { SubscriptionType } from 'uiSrc/constants/pubSub'

import { MessagesListWrapper, SubscriptionPanel } from './components'
import { MessagesListWrapper, PublishMessage, SubscriptionPanel } from './components'

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

Expand All @@ -27,7 +27,7 @@ const PubSubPage = () => {
</div>
</div>
<div className={styles.footerPanel}>
footer
<PublishMessage />
</div>
</div>
</>
Expand Down
2 changes: 2 additions & 0 deletions redisinsight/ui/src/pages/pubSub/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import SubscriptionPanel from './subscription-panel'
import MessagesListWrapper from './messages-list'
import PublishMessage from './publish-message'

export {
SubscriptionPanel,
MessagesListWrapper,
PublishMessage
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { fireEvent } from '@testing-library/react'
import { cloneDeep } from 'lodash'
import React from 'react'
import { publishMessage } from 'uiSrc/slices/pubsub/pubsub'
import { cleanup, clearStoreActions, mockedStore, render, screen } from 'uiSrc/utils/test-utils'

import PublishMessage from './PublishMessage'

let store: typeof mockedStore

beforeEach(() => {
cleanup()
store = cloneDeep(mockedStore)
store.clearActions()
})

describe('PublishMessage', () => {
it('should render', () => {
expect(render(<PublishMessage />)).toBeTruthy()
})

it('should dispatch subscribe action after submit', () => {
render(<PublishMessage />)
const expectedActions = [publishMessage()]
fireEvent.click(screen.getByTestId('publish-message-submit'))

expect(clearStoreActions(store.getActions())).toEqual(clearStoreActions(expectedActions))
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import {
EuiBadge,
EuiButton,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiForm,
EuiFormRow,
EuiIcon
} from '@elastic/eui'
import cx from 'classnames'
import React, { ChangeEvent, FormEvent, useEffect, useRef, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useParams } from 'react-router-dom'
import { appContextPubSub, setPubSubFieldsContext } from 'uiSrc/slices/app/context'
import { publishMessageAction } from 'uiSrc/slices/pubsub/pubsub'
import { ReactComponent as UserIcon } from 'uiSrc/assets/img/icons/user.svg'

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

const HIDE_BADGE_TIMER = 3000

const PublishMessage = () => {
const { channel: channelContext, message: messageContext } = useSelector(appContextPubSub)
const [channel, setChannel] = useState<string>(channelContext)
const [message, setMessage] = useState<string>(messageContext)
const [isShowBadge, setIsShowBadge] = useState<boolean>(false)
const [affectedClients, setAffectedClients] = useState<number>(0)

const fieldsRef = useRef({ channel, message })
const timeOutRef = useRef<NodeJS.Timeout>()

const { instanceId } = useParams<{ instanceId: string }>()
const dispatch = useDispatch()

useEffect(() => () => {
dispatch(setPubSubFieldsContext(fieldsRef.current))
timeOutRef.current && clearTimeout(timeOutRef.current)
}, [])

useEffect(() => {
fieldsRef.current = { channel, message }
}, [channel, message])

useEffect(() => {
if (isShowBadge) {
timeOutRef.current = setTimeout(() => {
isShowBadge && setIsShowBadge(false)
}, HIDE_BADGE_TIMER)

return
}

timeOutRef.current && clearTimeout(timeOutRef.current)
}, [isShowBadge])

const onSuccess = (affected: number) => {
setMessage('')
setAffectedClients(affected)
setIsShowBadge(true)
}

const onFormSubmit = (event: FormEvent<HTMLFormElement>): void => {
event.preventDefault()
setIsShowBadge(false)
dispatch(publishMessageAction(instanceId, channel, message, onSuccess))
}

return (
<EuiForm className={styles.container} component="form" onSubmit={onFormSubmit}>
<EuiFlexItem className={cx('flexItemNoFullWidth', 'inlineFieldsNoSpace')}>
<EuiFlexGroup gutterSize="none" alignItems="center" responsive={false}>
<EuiFlexItem className={styles.channelWrapper} grow>
<EuiFormRow fullWidth>
<EuiFieldText
fullWidth
name="channel"
id="channel"
placeholder="Enter Channel Name"
value={channel}
onChange={(e: ChangeEvent<HTMLInputElement>) => setChannel(e.target.value)}
autoComplete="off"
data-testid="field-channel-name"
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem className={styles.messageWrapper} grow>
<EuiFormRow fullWidth>
<>
<EuiFieldText
fullWidth
className={cx(styles.messageField, { [styles.showBadge]: isShowBadge })}
name="message"
id="message"
placeholder="Enter Message"
value={message}
onChange={(e: ChangeEvent<HTMLInputElement>) => setMessage(e.target.value)}
autoComplete="off"
data-testid="field-message"
/>
<EuiBadge className={cx(styles.badge, { [styles.show]: isShowBadge })} data-testid="affected-clients-badge">
<EuiIcon className={styles.iconCheckBadge} type="check" />
<span data-testid="affected-clients">{affectedClients}</span>
<EuiIcon className={styles.iconUserBadge} type={UserIcon || 'user'} />
</EuiBadge>
</>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexGroup responsive={false} justifyContent="flexEnd" style={{ marginTop: 6 }}>
<EuiFlexItem grow={false}>
<EuiButton
fill
color="secondary"
className="btn-add"
type="submit"
data-testid="publish-message-submit"
>
Publish
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiForm>
)
}

export default PublishMessage
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import PublishMessage from './PublishMessage'

export default PublishMessage
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
.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;
}

.iconCheckBadge {
margin-right: 6px;
}

.iconUserBadge {
color: var(--htmlColor) !important;
margin-bottom: 2px;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,44 +30,44 @@ const SubscriptionPanel = () => {
const displayMessages = count !== 0 || isSubscribed

return (
<div>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="none">
<EuiFlexItem grow={false} className={styles.iconSubscribe}>
<EuiIcon
className={styles.iconUser}
type={isSubscribed ? subscribedIcon : notSubscribedIcon}
/>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="s" responsive={false}>
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="none" responsive={false}>
<EuiFlexItem grow={false} className={styles.iconSubscribe}>
<EuiIcon
className={styles.iconUser}
type={isSubscribed ? subscribedIcon : notSubscribedIcon}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="s" data-testid="subscribe-status-text">
You are { !isSubscribed && 'not' } subscribed
</EuiText>
</EuiFlexItem>
{displayMessages && (
<EuiFlexItem grow={false} style={{ marginLeft: 12 }}>
<EuiText color="subdued" size="s" data-testid="messages-count">Messages: {count}</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="s">You are { !isSubscribed && 'not' } subscribed</EuiText>
</EuiFlexItem>
{displayMessages && (
<EuiFlexItem grow={false} style={{ marginLeft: 12 }}>
<EuiText color="subdued" size="s" data-testid="messages-count">Messages: {count}</EuiText>
</EuiFlexItem>
)}
</EuiFlexGroup>
)}
</EuiFlexGroup>

</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
size="s"
color="secondary"
className={styles.buttonSubscribe}
type="submit"
onClick={toggleSubscribe}
iconType={isSubscribed ? 'minusInCircle' : UserInCircle}
data-testid="btn-submit"
disabled={loading}
>
{ isSubscribed ? 'Unsubscribe' : 'Subscribe' }
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</div>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill={!isSubscribed}
size="s"
color="secondary"
className={styles.buttonSubscribe}
type="submit"
onClick={toggleSubscribe}
iconType={isSubscribed ? 'minusInCircle' : UserInCircle}
data-testid="btn-submit"
disabled={loading}
>
{ isSubscribed ? 'Unsubscribe' : 'Subscribe' }
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
)
}

Expand Down
3 changes: 1 addition & 2 deletions redisinsight/ui/src/pages/pubSub/styles.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@
}

.footerPanel {
height: 80px;
margin-top: 16px;
padding: 10px 18px;
padding: 10px 18px 28px;
}

.header {
Expand Down
11 changes: 11 additions & 0 deletions redisinsight/ui/src/slices/app/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ export const initialState: StateAppContext = {
panelSizes: {
vertical: {}
}
},
pubsub: {
channel: '',
message: ''
}
}

Expand Down Expand Up @@ -118,6 +122,10 @@ const appContextSlice = createSlice({
resetBrowserTree: (state) => {
state.browser.tree.selectedLeaf = {}
state.browser.tree.openNodes = {}
},
setPubSubFieldsContext: (state, { payload }: { payload: { channel: string, message: string } }) => {
state.pubsub.channel = payload.channel
state.pubsub.message = payload.message
}
},
})
Expand All @@ -142,6 +150,7 @@ export const {
setWorkbenchEAItem,
resetWorkbenchEAItem,
setWorkbenchEAItemScrollTop,
setPubSubFieldsContext
} = appContextSlice.actions

// Selectors
Expand All @@ -157,6 +166,8 @@ export const appContextSelectedKey = (state: RootState) =>
state.app.context.browser.keyList.selectedKey
export const appContextWorkbenchEA = (state: RootState) =>
state.app.context.workbench.enablementArea
export const appContextPubSub = (state: RootState) =>
state.app.context.pubsub

// The reducer
export default appContextSlice.reducer
4 changes: 4 additions & 0 deletions redisinsight/ui/src/slices/interfaces/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ export interface StateAppContext {
[key: string]: number
}
}
},
pubsub: {
channel: string
message: string
}
}

Expand Down
1 change: 1 addition & 0 deletions redisinsight/ui/src/slices/interfaces/pubsub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface PubSubMessage {

export interface StatePubSub {
loading: boolean
publishing: boolean
error: string
subscriptions: SubscriptionDto[]
isSubscribeTriggered: boolean
Expand Down
Loading