Skip to content

Commit 623030d

Browse files
#RI-3977 - add recommendation voting
1 parent af9e00d commit 623030d

File tree

16 files changed

+462
-5
lines changed

16 files changed

+462
-5
lines changed
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 4 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading

redisinsight/ui/src/constants/dbAnalysisRecommendations.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -593,7 +593,7 @@
593593
{
594594
"id": "2",
595595
"type": "span",
596-
"value": "was designed to help address your query needs and support a better development experience when dealing with complex data scenarios. Take a look at the "
596+
"value": " was designed to help address your query needs and support a better development experience when dealing with complex data scenarios. Take a look at the "
597597
},
598598
{
599599
"id": "3",

redisinsight/ui/src/constants/links.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export const EXTERNAL_LINKS = {
33
githubIssues: 'https://github.com/RedisInsight/RedisInsight/issues',
44
releaseNotes: 'https://github.com/RedisInsight/RedisInsight/releases',
55
userSurvey: 'https://www.surveymonkey.com/r/redisinsight',
6+
recommendationFeedback: 'https://github.com/RedisInsight/RedisInsight/issues/new/choose',
67
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export enum Vote {
2+
DoubleLike = 'amazing',
3+
Like = 'useful',
4+
Dislike = 'not useful'
5+
}

redisinsight/ui/src/pages/databaseAnalysis/components/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import AnalysisDataView from './analysis-data-view'
22
import ExpirationGroupsView from './analysis-ttl-view'
33
import EmptyAnalysisMessage from './empty-analysis-message'
44
import Header from './header'
5+
import RecommendationVoting from './recommendation-voting'
56
import SummaryPerData from './summary-per-data'
67
import TableLoader from './table-loader'
78
import TopKeys from './top-keys'
@@ -12,6 +13,7 @@ export {
1213
ExpirationGroupsView,
1314
EmptyAnalysisMessage,
1415
Header,
16+
RecommendationVoting,
1517
SummaryPerData,
1618
TableLoader,
1719
TopKeys,
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import React from 'react'
2+
import { cloneDeep } from 'lodash'
3+
import { instance, mock } from 'ts-mockito'
4+
import { setRecommendationVote } from 'uiSrc/slices/analytics/dbAnalysis'
5+
6+
import {
7+
cleanup,
8+
mockedStore,
9+
fireEvent,
10+
render,
11+
screen,
12+
waitForEuiPopoverVisible,
13+
} from 'uiSrc/utils/test-utils'
14+
15+
import RecommendationVoting, { Props } from './RecommendationVoting'
16+
17+
const mockedProps = mock<Props>()
18+
19+
let store: typeof mockedStore
20+
21+
beforeEach(() => {
22+
cleanup()
23+
store = cloneDeep(mockedStore)
24+
store.clearActions()
25+
})
26+
27+
jest.mock('uiSrc/telemetry', () => ({
28+
...jest.requireActual('uiSrc/telemetry'),
29+
sendEventTelemetry: jest.fn(),
30+
}))
31+
32+
jest.mock('react-redux', () => ({
33+
...jest.requireActual('react-redux'),
34+
useSelector: jest.fn(),
35+
}))
36+
37+
describe('RecommendationVoting', () => {
38+
it('should render', () => {
39+
expect(render(<RecommendationVoting {...instance(mockedProps)} />)).toBeTruthy()
40+
})
41+
42+
it('should call "setRecommendationVote" action be called after click "amazing-vote-btn"', () => {
43+
render(<RecommendationVoting {...instance(mockedProps)} />)
44+
fireEvent.click(screen.getByTestId('amazing-vote-btn'))
45+
46+
const expectedActions = [setRecommendationVote()]
47+
expect(store.getActions()).toEqual(expectedActions)
48+
})
49+
50+
it('should call "setRecommendationVote" action be called after click "useful-vote-btn"', () => {
51+
render(<RecommendationVoting {...instance(mockedProps)} />)
52+
fireEvent.click(screen.getByTestId('useful-vote-btn'))
53+
54+
const expectedActions = [setRecommendationVote()]
55+
expect(store.getActions()).toEqual(expectedActions)
56+
})
57+
58+
it('should call "setRecommendationVote" action be called after click "not-useful-vote-btn"', () => {
59+
render(<RecommendationVoting {...instance(mockedProps)} />)
60+
fireEvent.click(screen.getByTestId('not-useful-vote-btn'))
61+
62+
const expectedActions = [setRecommendationVote()]
63+
expect(store.getActions()).toEqual(expectedActions)
64+
})
65+
66+
it('should render popover after click "not-useful-vote-btn"', async () => {
67+
render(<RecommendationVoting {...instance(mockedProps)} />)
68+
69+
expect(document.querySelector('[data-test-subj="github-repo-link"]')).not.toBeInTheDocument()
70+
71+
fireEvent.click(screen.getByTestId('not-useful-vote-btn'))
72+
await waitForEuiPopoverVisible()
73+
74+
expect(document.querySelector('[data-test-subj="github-repo-link"]')).toHaveAttribute('href', 'https://github.com/RedisInsight/RedisInsight/issues/new/choose')
75+
})
76+
77+
it('should render component where all buttons are disabled"', async () => {
78+
render(<RecommendationVoting {...instance(mockedProps)} vote="amazing" />)
79+
80+
expect(screen.getByTestId('amazing-vote-btn')).toBeDisabled()
81+
expect(screen.getByTestId('useful-vote-btn')).toBeDisabled()
82+
expect(screen.getByTestId('not-useful-vote-btn')).toBeDisabled()
83+
})
84+
})
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import React, { useState } from 'react'
2+
import { useDispatch } from 'react-redux'
3+
import cx from 'classnames'
4+
import {
5+
EuiButton,
6+
EuiButtonIcon,
7+
EuiPopover,
8+
EuiText,
9+
EuiToolTip,
10+
EuiFlexGroup,
11+
EuiIcon,
12+
EuiLink,
13+
} from '@elastic/eui'
14+
import { putRecommendationVote } from 'uiSrc/slices/analytics/dbAnalysis'
15+
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
16+
import { EXTERNAL_LINKS } from 'uiSrc/constants/links'
17+
import { Vote } from 'uiSrc/constants/recommendations'
18+
19+
import { ReactComponent as LikeIcon } from 'uiSrc/assets/img/icons/like.svg'
20+
import { ReactComponent as DoubleLikeIcon } from 'uiSrc/assets/img/icons/double_like.svg'
21+
import { ReactComponent as DislikeIcon } from 'uiSrc/assets/img/icons/dislike.svg'
22+
import GithubSVG from 'uiSrc/assets/img/sidebar/github.svg'
23+
import styles from './styles.module.scss'
24+
25+
export interface Props { vote?: Vote, name: string }
26+
27+
const RecommendationVoting = ({ vote, name }: Props) => {
28+
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
29+
const dispatch = useDispatch()
30+
31+
const onSuccessVoted = (instanceId: string, name: string, vote: Vote) => {
32+
sendEventTelemetry({
33+
event: TelemetryEvent.DATABASE_ANALYSIS_RECOMMENDATIONS_VOTED,
34+
eventData: {
35+
databaseId: instanceId,
36+
name,
37+
vote,
38+
}
39+
})
40+
}
41+
42+
const handleClick = (name: string, vote: Vote) => {
43+
if (vote === Vote.Dislike) {
44+
setIsPopoverOpen(true)
45+
}
46+
dispatch(putRecommendationVote(name, vote, onSuccessVoted))
47+
}
48+
49+
return (
50+
<EuiFlexGroup alignItems="center" className={styles.votingContainer}>
51+
<EuiText size="m">Rate Recommendation</EuiText>
52+
<div className={styles.vote}>
53+
<EuiToolTip
54+
content="Amazing"
55+
position="bottom"
56+
>
57+
<EuiButtonIcon
58+
disabled={!!vote}
59+
iconType={DoubleLikeIcon}
60+
className={cx(styles.voteBtn, { [styles.selected]: vote === Vote.DoubleLike })}
61+
aria-label="vote amazing"
62+
data-testid="amazing-vote-btn"
63+
onClick={() => handleClick(name, Vote.DoubleLike)}
64+
/>
65+
</EuiToolTip>
66+
<EuiToolTip
67+
content="Useful"
68+
position="bottom"
69+
>
70+
<EuiButtonIcon
71+
disabled={!!vote}
72+
iconType={LikeIcon}
73+
className={cx(styles.voteBtn, { [styles.selected]: vote === Vote.Like })}
74+
aria-label="vote useful"
75+
data-testid="useful-vote-btn"
76+
onClick={() => handleClick(name, Vote.Like)}
77+
/>
78+
</EuiToolTip>
79+
<EuiToolTip
80+
content="Not Useful"
81+
position="bottom"
82+
>
83+
<EuiPopover
84+
initialFocus={false}
85+
anchorPosition="rightCenter"
86+
isOpen={isPopoverOpen}
87+
closePopover={() => setIsPopoverOpen(false)}
88+
anchorClassName={styles.popoverAnchor}
89+
panelClassName={cx('euiToolTip', 'popoverLikeTooltip', styles.popover)}
90+
button={(
91+
<EuiButtonIcon
92+
disabled={!!vote}
93+
iconType={DislikeIcon}
94+
className={cx(styles.voteBtn, { [styles.selected]: vote === Vote.Dislike })}
95+
aria-label="vote not useful"
96+
data-testid="not-useful-vote-btn"
97+
onClick={() => handleClick(name, Vote.Dislike)}
98+
/>
99+
)}
100+
>
101+
<div>
102+
Thank you for your feedback, Tell us how we can improve
103+
<EuiButton
104+
aria-label="recommendation feedback"
105+
fill
106+
data-testid="recommendation-feedback-btn"
107+
className={styles.feedbackBtn}
108+
color="secondary"
109+
size="s"
110+
>
111+
<EuiLink
112+
external={false}
113+
className={styles.link}
114+
href={EXTERNAL_LINKS.recommendationFeedback}
115+
target="_blank"
116+
data-test-subj="github-repo-link"
117+
>
118+
<EuiIcon
119+
className={styles.githubIcon}
120+
aria-label="redis insight github issues"
121+
type={GithubSVG}
122+
data-testid="github-repo-icon"
123+
/>
124+
To Github
125+
</EuiLink>
126+
</EuiButton>
127+
<EuiButtonIcon
128+
iconType="cross"
129+
color="primary"
130+
id="close-monitor"
131+
aria-label="close popover"
132+
data-testid="close-popover"
133+
className={styles.icon}
134+
onClick={() => setIsPopoverOpen(false)}
135+
/>
136+
</div>
137+
</EuiPopover>
138+
</EuiToolTip>
139+
</div>
140+
</EuiFlexGroup>
141+
)
142+
}
143+
144+
export default RecommendationVoting
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import RecommendationVoting from './RecommendationVoting'
2+
3+
export default RecommendationVoting

0 commit comments

Comments
 (0)