Skip to content

Commit fad2b9d

Browse files
Merge pull request #3462 from RedisInsight/fe/feature/RI-5156_rdi_tip
#RI-5156 - add internal link in recommendation
2 parents 8d40399 + 1c67976 commit fad2b9d

File tree

50 files changed

+781
-494
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+781
-494
lines changed

redisinsight/ui/src/components/index.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,17 @@ import ImportDatabasesDialog from './import-databases-dialog'
2323
import OnboardingTour from './onboarding-tour'
2424
import CodeBlock from './code-block'
2525
import ShowChildByCondition from './show-child-by-condition'
26-
import RecommendationVoting from './recommendation-voting'
27-
import RecommendationCopyComponent from './recommendation-copy-component'
2826
import FeatureFlagComponent from './feature-flag-component'
2927
import AutoRefresh from './auto-refresh'
3028
import { ModuleNotLoaded, FilterNotAvailable } from './messages'
3129
import RdiInstanceHeader from './rdi-instance-header'
30+
import {
31+
RecommendationBody,
32+
RecommendationBadges,
33+
RecommendationBadgesLegend,
34+
RecommendationCopyComponent,
35+
RecommendationVoting,
36+
} from './recommendation'
3237

3338
export { FullScreen } from './full-screen'
3439

@@ -71,4 +76,7 @@ export {
7176
FilterNotAvailable,
7277
AutoRefresh,
7378
RdiInstanceHeader,
79+
RecommendationBody,
80+
RecommendationBadges,
81+
RecommendationBadgesLegend,
7482
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import React from 'react'
2+
import { instance, mock } from 'ts-mockito'
3+
import { render } from 'uiSrc/utils/test-utils'
4+
import BadgeIcon, { Props } from './BadgeIcon'
5+
6+
const mockedProps = mock<Props>()
7+
8+
const icon = <div />
9+
10+
describe('BadgeIcon', () => {
11+
it('should render', () => {
12+
expect(render(<BadgeIcon {...instance(mockedProps)} icon={icon} />)).toBeTruthy()
13+
})
14+
})
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import React from 'react'
2+
import { EuiToolTip, EuiFlexItem } from '@elastic/eui'
3+
import styles from '../styles.module.scss'
4+
5+
export interface Props {
6+
id: string
7+
icon: React.ReactElement
8+
name: string
9+
}
10+
const BadgeIcon = ({ id, icon, name }: Props) => (
11+
<EuiFlexItem key={id} className={styles.badge} grow={false} data-testid={`recommendation-badge-${id}`}>
12+
<div data-testid={id} className={styles.badgeWrapper}>
13+
<EuiToolTip content={name} position="top" display="inlineBlock" anchorClassName="flex-row">
14+
{icon}
15+
</EuiToolTip>
16+
</div>
17+
</EuiFlexItem>
18+
)
19+
20+
export default BadgeIcon
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import BadgeIcon from './BadgeIcon'
2+
3+
export default BadgeIcon
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import React from 'react'
2+
import CodeIcon from 'uiSrc/assets/img/code-changes.svg?react'
3+
import ConfigurationIcon from 'uiSrc/assets/img/configuration-changes.svg?react'
4+
import UpgradeIcon from 'uiSrc/assets/img/upgrade.svg?react'
5+
6+
import styles from './styles.module.scss'
7+
8+
export const badgesContent = [
9+
{ id: 'code_changes', icon: <CodeIcon className={styles.badgeIcon} />, name: 'Code Changes' },
10+
{ id: 'configuration_changes', icon: <ConfigurationIcon className={styles.badgeIcon} />, name: 'Configuration Changes' },
11+
{ id: 'upgrade', icon: <UpgradeIcon className={styles.badgeIcon} />, name: 'Upgrade' },
12+
]
13+
14+
export const utmMedium = 'recommendation'
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import React from 'react'
2+
import { instance, mock } from 'ts-mockito'
3+
import { fireEvent, render, screen } from 'uiSrc/utils/test-utils'
4+
import ContentElement, { Props } from './ContentElement'
5+
6+
const mockedProps = mock<Props>()
7+
8+
const mockTelemetryName = 'name'
9+
10+
describe('ContentElement', () => {
11+
it('should render', () => {
12+
expect(render(<ContentElement {...instance(mockedProps)} />)).toBeTruthy()
13+
})
14+
15+
it('should render paragraph', () => {
16+
const mockContent = {
17+
type: 'paragraph',
18+
value: 'paragraph',
19+
}
20+
render(<ContentElement content={mockContent} telemetryName={mockTelemetryName} idx={0} />)
21+
22+
expect(screen.queryByTestId(`paragraph-${mockTelemetryName}-0`)).toBeInTheDocument()
23+
})
24+
25+
it('should render span', () => {
26+
const mockContent = {
27+
type: 'span',
28+
value: 'span',
29+
}
30+
render(<ContentElement content={mockContent} telemetryName={mockTelemetryName} idx={0} />)
31+
32+
expect(screen.queryByTestId(`span-${mockTelemetryName}-0`)).toBeInTheDocument()
33+
})
34+
35+
it('should render code', () => {
36+
const mockContent = {
37+
type: 'code',
38+
value: 'code',
39+
}
40+
render(<ContentElement content={mockContent} telemetryName={mockTelemetryName} idx={0} />)
41+
42+
expect(screen.queryByTestId(`code-${mockTelemetryName}-0`)).toBeInTheDocument()
43+
})
44+
45+
it('should render spacer', () => {
46+
const mockContent = {
47+
type: 'spacer',
48+
value: 'l',
49+
}
50+
render(<ContentElement content={mockContent} telemetryName={mockTelemetryName} idx={0} />)
51+
52+
expect(screen.queryByTestId(`spacer-${mockTelemetryName}-0`)).toBeInTheDocument()
53+
})
54+
55+
it('should render link', () => {
56+
const mockContent = {
57+
type: 'link',
58+
value: 'link',
59+
}
60+
render(<ContentElement content={mockContent} telemetryName={mockTelemetryName} idx={0} />)
61+
62+
expect(screen.queryByTestId(`link-${mockTelemetryName}-0`)).toBeInTheDocument()
63+
})
64+
65+
it('should render code link', () => {
66+
const mockContent = {
67+
type: 'code-link',
68+
value: 'link',
69+
}
70+
render(<ContentElement content={mockContent} telemetryName={mockTelemetryName} idx={0} />)
71+
72+
expect(screen.queryByTestId(`code-link-${mockTelemetryName}-0`)).toBeInTheDocument()
73+
})
74+
75+
it('should render link-sso', () => {
76+
const mockContent = {
77+
type: 'link-sso',
78+
value: 'link-sso',
79+
}
80+
render(<ContentElement content={mockContent} telemetryName={mockTelemetryName} idx={0} />)
81+
82+
expect(screen.queryByTestId(`link-sso-${mockTelemetryName}-0`)).toBeInTheDocument()
83+
})
84+
85+
it('should render internal-link', () => {
86+
const mockContent = {
87+
type: 'internal-link',
88+
value: {
89+
path: '/some-path',
90+
name: 'name',
91+
},
92+
}
93+
render(<ContentElement content={mockContent} telemetryName={mockTelemetryName} idx={0} />)
94+
95+
expect(screen.queryByTestId(`internal-link-${mockTelemetryName}-0`)).toBeInTheDocument()
96+
})
97+
98+
it('should render list', () => {
99+
const mockContent = {
100+
type: 'list',
101+
value: [[]],
102+
}
103+
render(<ContentElement content={mockContent} telemetryName={mockTelemetryName} idx={0} />)
104+
105+
expect(screen.queryByTestId(`list-${mockTelemetryName}-0`)).toBeInTheDocument()
106+
})
107+
108+
it('should render unknown', () => {
109+
const mockContent = {
110+
type: 'unknown',
111+
value: 'unknown',
112+
}
113+
render(<ContentElement content={mockContent} telemetryName={mockTelemetryName} idx={0} />)
114+
115+
expect(screen.getByText('unknown')).toBeInTheDocument()
116+
})
117+
118+
it('click on link should call onClick', () => {
119+
const onClickMock = jest.fn()
120+
const mockContent = {
121+
type: 'link',
122+
value: 'link',
123+
}
124+
125+
const { queryByTestId } = render(
126+
<ContentElement onLinkClick={onClickMock} content={mockContent} telemetryName={mockTelemetryName} idx={0} />
127+
)
128+
129+
fireEvent.click(queryByTestId(`link-${mockTelemetryName}-0`) as HTMLElement)
130+
131+
expect(onClickMock).toBeCalled()
132+
})
133+
})
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import React from 'react'
2+
import { isArray } from 'lodash'
3+
import { EuiTextColor, EuiLink, EuiSpacer } from '@elastic/eui'
4+
import { SpacerSize } from '@elastic/eui/src/components/spacer/spacer'
5+
import cx from 'classnames'
6+
import { OAuthSsoHandlerDialog, OAuthConnectFreeDb } from 'uiSrc/components'
7+
import { getUtmExternalLink } from 'uiSrc/utils/links'
8+
import { replaceVariables } from 'uiSrc/utils/recommendation'
9+
import { IRecommendationContent } from 'uiSrc/slices/interfaces/recommendations'
10+
import { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces'
11+
import InternalLink from '../internal-link'
12+
import RecommendationBody from '../recommendation-body'
13+
import { utmMedium } from '../constants'
14+
15+
import styles from '../styles.module.scss'
16+
17+
export interface Props {
18+
content: IRecommendationContent
19+
telemetryName: string
20+
params?: any
21+
onLinkClick?: () => void
22+
insights?: boolean
23+
idx: number
24+
}
25+
26+
const ContentElement = (props: Props) => {
27+
const { content = {}, params, onLinkClick, telemetryName, insights, idx } = props
28+
const { type, value, parameter } = content
29+
30+
const replacedValue = replaceVariables(value, parameter, params)
31+
32+
switch (type) {
33+
case 'paragraph':
34+
return (
35+
<EuiTextColor
36+
data-testid={`paragraph-${telemetryName}-${idx}`}
37+
key={`${telemetryName}-${idx}`}
38+
component="div"
39+
className={cx(styles.text, { [styles.insights]: insights })}
40+
color="subdued"
41+
>
42+
{value}
43+
</EuiTextColor>
44+
)
45+
case 'code':
46+
return (
47+
<EuiTextColor
48+
data-testid={`code-${telemetryName}-${idx}`}
49+
className={cx(styles.code, { [styles.insights]: insights })}
50+
key={`${telemetryName}-${idx}`}
51+
color="subdued"
52+
>
53+
<code className={cx(styles.span, styles.text)}>
54+
{value}
55+
</code>
56+
</EuiTextColor>
57+
)
58+
case 'span':
59+
return (
60+
<EuiTextColor
61+
data-testid={`span-${telemetryName}-${idx}`}
62+
key={`${telemetryName}-${idx}`}
63+
color="subdued"
64+
className={cx(styles.span, styles.text, { [styles.insights]: insights })}
65+
>
66+
{value}
67+
</EuiTextColor>
68+
)
69+
case 'link':
70+
return (
71+
<EuiLink
72+
key={`${telemetryName}-${idx}`}
73+
external={false}
74+
data-testid={`link-${telemetryName}-${idx}`}
75+
target="_blank"
76+
href={getUtmExternalLink(value.href, { medium: utmMedium, campaign: telemetryName })}
77+
onClick={() => onLinkClick?.()}
78+
>
79+
{value.name}
80+
</EuiLink>
81+
)
82+
case 'link-sso':
83+
return (
84+
<OAuthSsoHandlerDialog>
85+
{(ssoCloudHandlerClick) => (
86+
<EuiLink
87+
key={`${telemetryName}-${idx}`}
88+
external={false}
89+
data-testid={`link-sso-${telemetryName}-${idx}`}
90+
target="_blank"
91+
onClick={(e) => {
92+
ssoCloudHandlerClick?.(e, {
93+
source: telemetryName as OAuthSocialSource,
94+
action: OAuthSocialAction.Create
95+
})
96+
}}
97+
href={getUtmExternalLink(value.href, { medium: utmMedium, campaign: telemetryName })}
98+
>
99+
{value.name}
100+
</EuiLink>
101+
)}
102+
</OAuthSsoHandlerDialog>
103+
)
104+
case 'connect-btn':
105+
return (
106+
<OAuthConnectFreeDb source={telemetryName as OAuthSocialSource} />
107+
)
108+
case 'code-link':
109+
return (
110+
<EuiLink
111+
key={`${telemetryName}-${idx}`}
112+
external={false}
113+
data-testid={`code-link-${telemetryName}-${idx}`}
114+
target="_blank"
115+
href={getUtmExternalLink(value.href, { medium: utmMedium, campaign: telemetryName })}
116+
>
117+
<EuiTextColor
118+
className={cx(styles.code, { [styles.insights]: insights })}
119+
color="subdued"
120+
>
121+
<code className={cx(styles.span, styles.text)}>
122+
{value.name}
123+
</code>
124+
</EuiTextColor>
125+
</EuiLink>
126+
)
127+
case 'spacer':
128+
return (
129+
<EuiSpacer
130+
data-testid={`spacer-${telemetryName}-${idx}`}
131+
key={`${telemetryName}-${idx}`}
132+
size={value as SpacerSize}
133+
/>
134+
)
135+
case 'list':
136+
return (
137+
<ul
138+
className={styles.list}
139+
data-testid={`list-${telemetryName}-${idx}`}
140+
key={`${telemetryName}-${idx}`}
141+
>
142+
{isArray(value) && value.map((listElement: IRecommendationContent[], idx: number) => (
143+
<li
144+
className={cx(styles.listItem, { [styles.insights]: insights })}
145+
// eslint-disable-next-line react/no-array-index-key
146+
key={`list-item-${idx}`}
147+
>
148+
<RecommendationBody
149+
elements={listElement}
150+
params={params}
151+
telemetryName={telemetryName}
152+
onLinkClick={onLinkClick}
153+
insights={insights}
154+
/>
155+
</li>
156+
))}
157+
</ul>
158+
)
159+
case 'internal-link':
160+
return (
161+
<InternalLink
162+
key={`${telemetryName}-${idx}`}
163+
dataTestid={`internal-link-${telemetryName}-${idx}`}
164+
path={replacedValue.path}
165+
text={replacedValue.name}
166+
/>
167+
)
168+
default:
169+
return value
170+
}
171+
}
172+
173+
export default ContentElement
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import ContentElement from './ContentElement'
2+
3+
export default ContentElement

0 commit comments

Comments
 (0)