Skip to content

Commit 4e10b08

Browse files
Merge pull request #4258 from RedisInsight/fe/feature/RI-6371-instance-navigation
Fe/feature/ri 6371 instance navigation
2 parents 6433717 + 692a4a6 commit 4e10b08

File tree

26 files changed

+820
-36
lines changed

26 files changed

+820
-36
lines changed
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading

redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import { cleanup, mockedStore, render, screen, fireEvent, initialStateDefault, m
66
import {
77
checkDatabaseIndex,
88
connectedInstanceInfoSelector,
9-
connectedInstanceSelector
9+
connectedInstanceSelector,
10+
loadInstances,
1011
} from 'uiSrc/slices/instances/instances'
12+
import { loadInstances as loadRdiInstances } from 'uiSrc/slices/rdi/instances'
1113
import { appContextDbIndex } from 'uiSrc/slices/app/context'
1214

1315
import { FeatureFlags } from 'uiSrc/constants'

redisinsight/ui/src/components/instance-header/InstanceHeader.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'
3030
import { isAnyFeatureEnabled } from 'uiSrc/utils/features'
3131
import { getConfig } from 'uiSrc/config'
3232
import { appReturnUrlSelector } from 'uiSrc/slices/app/url-handling'
33+
import InstancesNavigationPopover from './components/instances-navigation-popover'
3334
import styles from './styles.module.scss'
3435

3536
const riConfig = getConfig()
@@ -70,6 +71,8 @@ const InstanceHeader = ({ onChangeDbIndex }: Props) => {
7071

7172
useEffect(() => { setDbIndex(String(db || 0)) }, [db])
7273

74+
const isRedisStack = server?.buildType === BuildType.RedisStack
75+
7376
const goHome = () => {
7477
history.push(Pages.home)
7578
}
@@ -160,7 +163,11 @@ const InstanceHeader = ({ onChangeDbIndex }: Props) => {
160163
/>
161164
)}
162165
<EuiFlexItem style={{ overflow: 'hidden' }}>
163-
<b className={styles.dbName}>{name}</b>
166+
{isRedisStack ? (
167+
<b className={styles.dbName}>{name}</b>
168+
) : (
169+
<InstancesNavigationPopover name={name} />
170+
)}
164171
</EuiFlexItem>
165172
{databases > 1 && (
166173
<EuiFlexItem style={{ padding: '4px 0 4px 12px' }} grow={false}>
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { cloneDeep } from 'lodash'
2+
import React from 'react'
3+
import { act } from 'react-dom/test-utils'
4+
import { cleanup, fireEvent, mockedStore, render, screen } from 'uiSrc/utils/test-utils'
5+
import { instancesSelector } from 'uiSrc/slices/rdi/instances'
6+
import { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'
7+
import InstancesNavigationPopover, { InstancesTabs } from './InstancesNavigationPopover'
8+
9+
let store: typeof mockedStore
10+
beforeEach(() => {
11+
cleanup()
12+
store = cloneDeep(mockedStore)
13+
store.clearActions()
14+
})
15+
16+
const mockRdis = [
17+
{
18+
id: 'rdiDB_1',
19+
name: 'RdiDB_1'
20+
},
21+
{
22+
id: 'rdiDB_2',
23+
name: 'RdiDB_2'
24+
}
25+
]
26+
27+
const mockDbs = [
28+
{
29+
id: 'db_1',
30+
name: 'DB_1'
31+
},
32+
{
33+
id: 'db_2',
34+
name: 'DB_2'
35+
}
36+
]
37+
38+
jest.mock('uiSrc/slices/rdi/instances', () => ({
39+
...jest.requireActual('uiSrc/slices/rdi/instances'),
40+
instancesSelector: jest.fn().mockReturnValue({
41+
data: mockRdis,
42+
connectedInstance: {
43+
id: 'rdiDB_1',
44+
name: 'RdiDB_1'
45+
},
46+
})
47+
}))
48+
49+
jest.mock('uiSrc/slices/instances/instances', () => ({
50+
...jest.requireActual('uiSrc/slices/instances/instances'),
51+
instancesSelector: jest.fn().mockReturnValue({
52+
data: mockDbs,
53+
connectedInstance: {
54+
id: 'db_1',
55+
name: 'DB_1'
56+
},
57+
})
58+
}))
59+
60+
jest.mock('uiSrc/telemetry', () => ({
61+
...jest.requireActual('uiSrc/telemetry'),
62+
sendEventTelemetry: jest.fn(),
63+
}))
64+
65+
describe('InstancesNavigationPopover', () => {
66+
it('should render', () => {
67+
expect(render(<InstancesNavigationPopover name="db" />)).toBeTruthy()
68+
})
69+
70+
it('should open popover on click', () => {
71+
render(<InstancesNavigationPopover name="db" />)
72+
73+
act(() => {
74+
fireEvent.click(screen.getByTestId('nav-instance-popover-btn'))
75+
})
76+
77+
expect(screen.getByTestId('instances-tabs-testId')).toBeInTheDocument()
78+
})
79+
80+
it('should filter instances list', () => {
81+
(instancesSelector as jest.Mock).mockReturnValue({
82+
data: mockRdis,
83+
})
84+
render(<InstancesNavigationPopover name="db" />)
85+
86+
act(() => {
87+
fireEvent.click(screen.getByTestId('nav-instance-popover-btn'))
88+
})
89+
90+
const searchInput = screen.getByTestId('instances-nav-popover-search')
91+
92+
expect(screen.getByText('RdiDB_2')).toBeInTheDocument()
93+
94+
fireEvent.change(searchInput, { target: { value: '_1' } })
95+
96+
expect(screen.getByText('RdiDB_1')).toBeInTheDocument()
97+
expect(screen.queryAllByText('RdiDB_2')).toHaveLength(0)
98+
})
99+
100+
it('should change tabs on tabs click', () => {
101+
render(<InstancesNavigationPopover name="db" />)
102+
103+
act(() => {
104+
fireEvent.click(screen.getByTestId('nav-instance-popover-btn'))
105+
})
106+
107+
expect(screen.getByTestId('instances-tabs-testId')).toBeInTheDocument()
108+
109+
act(() => {
110+
fireEvent.click(screen.getByTestId(`${InstancesTabs.RDI}-tab-id`))
111+
})
112+
expect(screen.getByText('Redis Data Integration page')).toBeInTheDocument()
113+
114+
act(() => {
115+
fireEvent.click(screen.getByTestId(`${InstancesTabs.Databases}-tab-id`))
116+
})
117+
118+
expect(screen.getByText('Redis Databases page')).toBeInTheDocument()
119+
})
120+
121+
it('should send event telemetry', () => {
122+
const sendEventTelemetryMock = jest.fn();
123+
(sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock)
124+
125+
render(<InstancesNavigationPopover name="db" />)
126+
127+
act(() => {
128+
fireEvent.click(screen.getByTestId('nav-instance-popover-btn'))
129+
})
130+
131+
expect(sendEventTelemetry).toBeCalledWith({
132+
event: TelemetryEvent.NAVIGATION_PANEL_OPENED,
133+
eventData: {
134+
databaseId: 'instanceId',
135+
numOfRdiDbs: 2,
136+
numOfRedisDbs: 0,
137+
}
138+
})
139+
})
140+
})
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import React, { ChangeEvent, useEffect, useState } from 'react'
2+
import { EuiFieldText, EuiIcon, EuiPopover, EuiSpacer, EuiTab, EuiTabs, EuiText } from '@elastic/eui'
3+
import cx from 'classnames'
4+
import { useSelector } from 'react-redux'
5+
import { useHistory, useParams } from 'react-router-dom'
6+
import { instancesSelector as rdiInstancesSelector } from 'uiSrc/slices/rdi/instances'
7+
import { instancesSelector as dbInstancesSelector } from 'uiSrc/slices/instances/instances'
8+
import Divider from 'uiSrc/components/divider/Divider'
9+
import { Pages } from 'uiSrc/constants'
10+
import Down from 'uiSrc/assets/img/Down.svg?react'
11+
import Search from 'uiSrc/assets/img/Search.svg'
12+
import { Instance, RdiInstance } from 'uiSrc/slices/interfaces'
13+
import { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'
14+
import { getDbIndex } from 'uiSrc/utils'
15+
import InstancesList from './components/instances-list'
16+
import styles from './styles.module.scss'
17+
18+
export interface Props {
19+
name: string
20+
}
21+
22+
export enum InstancesTabs {
23+
Databases = 'Databases',
24+
RDI = 'Redis Data Integration'
25+
}
26+
27+
const InstancesNavigationPopover = ({ name }: Props) => {
28+
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
29+
const [searchFilter, setSearchFilter] = useState('')
30+
const [filteredDbInstances, setFilteredDbInstances] = useState<Instance[]>([])
31+
const [filteredRdiInstances, setFilteredRdiInstances] = useState<RdiInstance[]>([])
32+
33+
const { instanceId, rdiInstanceId } = useParams<{ instanceId: string, rdiInstanceId: string }>()
34+
const [selectedTab, setSelectedTab] = useState(rdiInstanceId ? InstancesTabs.RDI : InstancesTabs.Databases)
35+
36+
const { data: rdiInstances } = useSelector(rdiInstancesSelector)
37+
const { data: dbInstances } = useSelector(dbInstancesSelector)
38+
const history = useHistory()
39+
40+
useEffect(() => {
41+
const dbFiltered = dbInstances?.filter((db) => {
42+
const label = `${db.name} ${getDbIndex(db.db)}`
43+
return label.toLowerCase?.().includes(searchFilter)
44+
})
45+
const rdiFiltered = rdiInstances?.filter((rdi) => rdi.name?.toLowerCase?.().includes(searchFilter))
46+
setFilteredDbInstances(dbFiltered)
47+
setFilteredRdiInstances(rdiFiltered)
48+
}, [dbInstances, rdiInstances, searchFilter])
49+
50+
const handleSearch = (e: ChangeEvent<HTMLInputElement>) => {
51+
const { value } = e.target
52+
setSearchFilter(value)
53+
}
54+
55+
const showPopover = () => {
56+
if (!isPopoverOpen) {
57+
sendEventTelemetry({
58+
event: TelemetryEvent.NAVIGATION_PANEL_OPENED,
59+
eventData: {
60+
databaseId: instanceId || rdiInstanceId,
61+
numOfRedisDbs: dbInstances?.length || 0,
62+
numOfRdiDbs: rdiInstances?.length || 0,
63+
}
64+
})
65+
}
66+
setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen)
67+
}
68+
69+
const btnLabel = selectedTab === InstancesTabs.Databases ? 'Redis Databases page' : 'Redis Data Integration page'
70+
71+
const goHome = () => {
72+
history.push(selectedTab === InstancesTabs.Databases ? Pages.home : Pages.rdi)
73+
}
74+
75+
return (
76+
<EuiPopover
77+
ownFocus
78+
anchorPosition="downRight"
79+
panelPaddingSize="none"
80+
isOpen={isPopoverOpen}
81+
closePopover={() => showPopover()}
82+
button={(
83+
<EuiText
84+
className={styles.showPopoverBtn}
85+
onClick={() => showPopover()}
86+
data-testid="nav-instance-popover-btn"
87+
>
88+
<b className={styles.breadCrumbLink}>{name}</b>
89+
<span>
90+
<EuiIcon
91+
color="primaryText"
92+
type={Down}
93+
/>
94+
</span>
95+
</EuiText>
96+
)}
97+
>
98+
<div className={styles.wrapper}>
99+
<div className={styles.searchInputContainer}>
100+
<EuiFieldText
101+
fullWidth
102+
className={styles.searchInput}
103+
icon={Search}
104+
value={searchFilter}
105+
onChange={(e) => handleSearch(e)}
106+
data-testid="instances-nav-popover-search"
107+
/>
108+
</div>
109+
<div>
110+
<div className={styles.tabsContainer}>
111+
<EuiTabs
112+
className={cx('tabs-active-borders', styles.tabs)}
113+
data-testid="instances-tabs-testId"
114+
>
115+
<EuiTab
116+
className={styles.tab}
117+
isSelected={selectedTab === InstancesTabs.Databases}
118+
onClick={() => setSelectedTab(InstancesTabs.Databases)}
119+
data-testid={`${InstancesTabs.Databases}-tab-id`}
120+
>{InstancesTabs.Databases} ({dbInstances?.length || 0})
121+
</EuiTab>
122+
123+
<EuiTab
124+
className={styles.tab}
125+
isSelected={selectedTab === InstancesTabs.RDI}
126+
onClick={() => setSelectedTab(InstancesTabs.RDI)}
127+
data-testid={`${InstancesTabs.RDI}-tab-id`}
128+
>{InstancesTabs.RDI} ({rdiInstances?.length || 0})
129+
</EuiTab>
130+
</EuiTabs>
131+
</div>
132+
<EuiSpacer size="m" />
133+
<InstancesList
134+
selectedTab={selectedTab}
135+
filteredDbInstances={filteredDbInstances}
136+
filteredRdiInstances={filteredRdiInstances}
137+
/>
138+
<div>
139+
<EuiSpacer size="m" />
140+
<Divider />
141+
<div className={styles.footerContainer}>
142+
<EuiText
143+
className={styles.homePageLink}
144+
onClick={goHome}
145+
>{btnLabel}
146+
</EuiText>
147+
</div>
148+
</div>
149+
</div>
150+
</div>
151+
</EuiPopover>
152+
)
153+
}
154+
155+
export default InstancesNavigationPopover

0 commit comments

Comments
 (0)