Skip to content

Commit 5dc4017

Browse files
committed
#RI-3035 - publish pub/sub messages panel
1 parent 7adf4b5 commit 5dc4017

File tree

18 files changed

+460
-44
lines changed

18 files changed

+460
-44
lines changed

redisinsight/ui/src/constants/api.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ enum ApiEndpoints {
6868

6969
SLOW_LOGS = 'slow-logs',
7070
SLOW_LOGS_CONFIG = 'slow-logs/config',
71+
72+
PUB_SUB = 'pub-sub',
73+
PUB_SUB_MESSAGES = 'pub-sub/messages'
7174
}
7275

7376
export const DEFAULT_SEARCH_MATCH = '*'

redisinsight/ui/src/pages/pubSub/PubSubPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React from 'react'
33
import InstanceHeader from 'uiSrc/components/instance-header'
44
import { SubscriptionType } from 'uiSrc/constants/pubSub'
55

6-
import { MessagesListWrapper, SubscriptionPanel } from './components'
6+
import { MessagesListWrapper, PublishMessage, SubscriptionPanel } from './components'
77

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

@@ -27,7 +27,7 @@ const PubSubPage = () => {
2727
</div>
2828
</div>
2929
<div className={styles.footerPanel}>
30-
footer
30+
<PublishMessage />
3131
</div>
3232
</div>
3333
</>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import SubscriptionPanel from './subscription-panel'
22
import MessagesListWrapper from './messages-list'
3+
import PublishMessage from './publish-message'
34

45
export {
56
SubscriptionPanel,
67
MessagesListWrapper,
8+
PublishMessage
79
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { fireEvent } from '@testing-library/react'
2+
import { cloneDeep } from 'lodash'
3+
import React from 'react'
4+
import { publishMessage } from 'uiSrc/slices/pubsub/pubsub'
5+
import { cleanup, clearStoreActions, mockedStore, render, screen } from 'uiSrc/utils/test-utils'
6+
7+
import PublishMessage from './PublishMessage'
8+
9+
let store: typeof mockedStore
10+
11+
beforeEach(() => {
12+
cleanup()
13+
store = cloneDeep(mockedStore)
14+
store.clearActions()
15+
})
16+
17+
describe('PublishMessage', () => {
18+
it('should render', () => {
19+
expect(render(<PublishMessage />)).toBeTruthy()
20+
})
21+
22+
it('should dispatch subscribe action after submit', () => {
23+
render(<PublishMessage />)
24+
const expectedActions = [publishMessage()]
25+
fireEvent.click(screen.getByTestId('publish-message-submit'))
26+
27+
expect(clearStoreActions(store.getActions())).toEqual(clearStoreActions(expectedActions))
28+
})
29+
})
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import {
2+
EuiBadge,
3+
EuiButton,
4+
EuiFieldText,
5+
EuiFlexGroup,
6+
EuiFlexItem,
7+
EuiForm,
8+
EuiFormRow,
9+
EuiIcon
10+
} from '@elastic/eui'
11+
import cx from 'classnames'
12+
import React, { ChangeEvent, FormEvent, useEffect, useRef, useState } from 'react'
13+
import { useDispatch, useSelector } from 'react-redux'
14+
import { useParams } from 'react-router-dom'
15+
import { appContextPubSub, setPubSubFieldsContext } from 'uiSrc/slices/app/context'
16+
import { publishMessageAction } from 'uiSrc/slices/pubsub/pubsub'
17+
import { ReactComponent as UserIcon } from 'uiSrc/assets/img/icons/user.svg'
18+
19+
import styles from './styles.module.scss'
20+
21+
const HIDE_BADGE_TIMER = 3000
22+
23+
const PublishMessage = () => {
24+
const { channel: channelContext, message: messageContext } = useSelector(appContextPubSub)
25+
const [channel, setChannel] = useState<string>(channelContext)
26+
const [message, setMessage] = useState<string>(messageContext)
27+
const [isShowBadge, setIsShowBadge] = useState<boolean>(false)
28+
const [affectedClients, setAffectedClients] = useState<number>(0)
29+
30+
const fieldsRef = useRef({ channel, message })
31+
const timeOutRef = useRef<NodeJS.Timeout>()
32+
33+
const { instanceId } = useParams<{ instanceId: string }>()
34+
const dispatch = useDispatch()
35+
36+
useEffect(() => () => {
37+
dispatch(setPubSubFieldsContext(fieldsRef.current))
38+
timeOutRef.current && clearTimeout(timeOutRef.current)
39+
}, [])
40+
41+
useEffect(() => {
42+
fieldsRef.current = { channel, message }
43+
}, [channel, message])
44+
45+
useEffect(() => {
46+
if (isShowBadge) {
47+
timeOutRef.current = setTimeout(() => {
48+
isShowBadge && setIsShowBadge(false)
49+
}, HIDE_BADGE_TIMER)
50+
51+
return
52+
}
53+
54+
timeOutRef.current && clearTimeout(timeOutRef.current)
55+
}, [isShowBadge])
56+
57+
const onSuccess = (affected: number) => {
58+
setMessage('')
59+
setAffectedClients(affected)
60+
setIsShowBadge(true)
61+
}
62+
63+
const onFormSubmit = (event: FormEvent<HTMLFormElement>): void => {
64+
event.preventDefault()
65+
setIsShowBadge(false)
66+
dispatch(publishMessageAction(instanceId, channel, message, onSuccess))
67+
}
68+
69+
return (
70+
<EuiForm className={styles.container} component="form" onSubmit={onFormSubmit}>
71+
<EuiFlexItem className={cx('flexItemNoFullWidth', 'inlineFieldsNoSpace')}>
72+
<EuiFlexGroup gutterSize="none" alignItems="center" responsive={false}>
73+
<EuiFlexItem className={styles.channelWrapper} grow>
74+
<EuiFormRow fullWidth>
75+
<EuiFieldText
76+
fullWidth
77+
name="channel"
78+
id="channel"
79+
placeholder="Enter Channel Name"
80+
value={channel}
81+
onChange={(e: ChangeEvent<HTMLInputElement>) => setChannel(e.target.value)}
82+
autoComplete="off"
83+
data-testid="field-channel-name"
84+
/>
85+
</EuiFormRow>
86+
</EuiFlexItem>
87+
<EuiFlexItem className={styles.messageWrapper} grow>
88+
<EuiFormRow fullWidth>
89+
<>
90+
<EuiFieldText
91+
fullWidth
92+
className={cx(styles.messageField, { [styles.showBadge]: isShowBadge })}
93+
name="message"
94+
id="message"
95+
placeholder="Enter Message"
96+
value={message}
97+
onChange={(e: ChangeEvent<HTMLInputElement>) => setMessage(e.target.value)}
98+
autoComplete="off"
99+
data-testid="field-message"
100+
/>
101+
<EuiBadge className={cx(styles.badge, { [styles.show]: isShowBadge })} data-testid="affected-clients-badge">
102+
<EuiIcon className={styles.iconCheckBadge} type="check" />
103+
<span data-testid="affected-clients">{affectedClients}</span>
104+
<EuiIcon className={styles.iconUserBadge} type={UserIcon || 'user'} />
105+
</EuiBadge>
106+
</>
107+
</EuiFormRow>
108+
</EuiFlexItem>
109+
</EuiFlexGroup>
110+
</EuiFlexItem>
111+
<EuiFlexGroup responsive={false} justifyContent="flexEnd" style={{ marginTop: 6 }}>
112+
<EuiFlexItem grow={false}>
113+
<EuiButton
114+
fill
115+
color="secondary"
116+
className="btn-add"
117+
type="submit"
118+
data-testid="publish-message-submit"
119+
>
120+
Publish
121+
</EuiButton>
122+
</EuiFlexItem>
123+
</EuiFlexGroup>
124+
</EuiForm>
125+
)
126+
}
127+
128+
export default PublishMessage
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import PublishMessage from './PublishMessage'
2+
3+
export default PublishMessage
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
.container {
2+
.channelWrapper {
3+
min-width: 180px;
4+
}
5+
.messageWrapper {
6+
flex-grow: 3 !important;
7+
position: relative;
8+
9+
.messageField {
10+
&.showBadge {
11+
padding-right: 80px;
12+
}
13+
}
14+
}
15+
16+
.badge {
17+
position: absolute;
18+
background-color: var(--pubSubClientsBadge) !important;
19+
top: 50%;
20+
right: 8px;
21+
transform: translateY(-50%);
22+
color: var(--htmlColor) !important;
23+
opacity: 0;
24+
pointer-events: none;
25+
transition: opacity 250ms ease-in-out;
26+
27+
&.show {
28+
opacity: 1;
29+
pointer-events: auto;
30+
}
31+
32+
:global(.euiBadge__text) {
33+
display: flex;
34+
align-items: center;
35+
}
36+
37+
.iconCheckBadge {
38+
margin-right: 6px;
39+
}
40+
41+
.iconUserBadge {
42+
color: var(--htmlColor) !important;
43+
margin-bottom: 2px;
44+
}
45+
}
46+
}

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

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -30,44 +30,44 @@ const SubscriptionPanel = () => {
3030
const displayMessages = count !== 0 || isSubscribed
3131

3232
return (
33-
<div>
34-
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="s">
35-
<EuiFlexItem grow={false}>
36-
<EuiFlexGroup alignItems="center" gutterSize="none">
37-
<EuiFlexItem grow={false} className={styles.iconSubscribe}>
38-
<EuiIcon
39-
className={styles.iconUser}
40-
type={isSubscribed ? subscribedIcon : notSubscribedIcon}
41-
/>
33+
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="s" responsive={false}>
34+
<EuiFlexItem grow={false}>
35+
<EuiFlexGroup alignItems="center" gutterSize="none" responsive={false}>
36+
<EuiFlexItem grow={false} className={styles.iconSubscribe}>
37+
<EuiIcon
38+
className={styles.iconUser}
39+
type={isSubscribed ? subscribedIcon : notSubscribedIcon}
40+
/>
41+
</EuiFlexItem>
42+
<EuiFlexItem grow={false}>
43+
<EuiText color="subdued" size="s" data-testid="subscribe-status-text">
44+
You are { !isSubscribed && 'not' } subscribed
45+
</EuiText>
46+
</EuiFlexItem>
47+
{displayMessages && (
48+
<EuiFlexItem grow={false} style={{ marginLeft: 12 }}>
49+
<EuiText color="subdued" size="s" data-testid="messages-count">Messages: {count}</EuiText>
4250
</EuiFlexItem>
43-
<EuiFlexItem grow={false}>
44-
<EuiText color="subdued" size="s">You are { !isSubscribed && 'not' } subscribed</EuiText>
45-
</EuiFlexItem>
46-
{displayMessages && (
47-
<EuiFlexItem grow={false} style={{ marginLeft: 12 }}>
48-
<EuiText color="subdued" size="s" data-testid="messages-count">Messages: {count}</EuiText>
49-
</EuiFlexItem>
50-
)}
51-
</EuiFlexGroup>
51+
)}
52+
</EuiFlexGroup>
5253

53-
</EuiFlexItem>
54-
<EuiFlexItem grow={false}>
55-
<EuiButton
56-
fill
57-
size="s"
58-
color="secondary"
59-
className={styles.buttonSubscribe}
60-
type="submit"
61-
onClick={toggleSubscribe}
62-
iconType={isSubscribed ? 'minusInCircle' : UserInCircle}
63-
data-testid="btn-submit"
64-
disabled={loading}
65-
>
66-
{ isSubscribed ? 'Unsubscribe' : 'Subscribe' }
67-
</EuiButton>
68-
</EuiFlexItem>
69-
</EuiFlexGroup>
70-
</div>
54+
</EuiFlexItem>
55+
<EuiFlexItem grow={false}>
56+
<EuiButton
57+
fill={!isSubscribed}
58+
size="s"
59+
color="secondary"
60+
className={styles.buttonSubscribe}
61+
type="submit"
62+
onClick={toggleSubscribe}
63+
iconType={isSubscribed ? 'minusInCircle' : UserInCircle}
64+
data-testid="btn-submit"
65+
disabled={loading}
66+
>
67+
{ isSubscribed ? 'Unsubscribe' : 'Subscribe' }
68+
</EuiButton>
69+
</EuiFlexItem>
70+
</EuiFlexGroup>
7171
)
7272
}
7373

redisinsight/ui/src/pages/pubSub/styles.module.scss

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,8 @@
1818
}
1919

2020
.footerPanel {
21-
height: 80px;
2221
margin-top: 16px;
23-
padding: 10px 18px;
22+
padding: 10px 18px 28px;
2423
}
2524

2625
.header {

redisinsight/ui/src/slices/app/context.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ export const initialState: StateAppContext = {
3131
panelSizes: {
3232
vertical: {}
3333
}
34+
},
35+
pubsub: {
36+
channel: '',
37+
message: ''
3438
}
3539
}
3640

@@ -118,6 +122,10 @@ const appContextSlice = createSlice({
118122
resetBrowserTree: (state) => {
119123
state.browser.tree.selectedLeaf = {}
120124
state.browser.tree.openNodes = {}
125+
},
126+
setPubSubFieldsContext: (state, { payload }: { payload: { channel: string, message: string } }) => {
127+
state.pubsub.channel = payload.channel
128+
state.pubsub.message = payload.message
121129
}
122130
},
123131
})
@@ -142,6 +150,7 @@ export const {
142150
setWorkbenchEAItem,
143151
resetWorkbenchEAItem,
144152
setWorkbenchEAItemScrollTop,
153+
setPubSubFieldsContext
145154
} = appContextSlice.actions
146155

147156
// Selectors
@@ -157,6 +166,8 @@ export const appContextSelectedKey = (state: RootState) =>
157166
state.app.context.browser.keyList.selectedKey
158167
export const appContextWorkbenchEA = (state: RootState) =>
159168
state.app.context.workbench.enablementArea
169+
export const appContextPubSub = (state: RootState) =>
170+
state.app.context.pubsub
160171

161172
// The reducer
162173
export default appContextSlice.reducer

0 commit comments

Comments
 (0)