Skip to content

RI-7365: Show no indexes message #4870

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { renderHook, act } from '@testing-library/react-hooks'
import { Pages } from 'uiSrc/constants'
import useStartWizard from './useStartWizard'

describe('useStartWizard', () => {
let mockPush: jest.Mock
let useHistoryMock: jest.SpyInstance
let useParamsMock: jest.SpyInstance

beforeEach(() => {
jest.clearAllMocks()
mockPush = jest.fn()

useHistoryMock = jest.spyOn(require('react-router-dom'), 'useHistory')
useHistoryMock.mockImplementation(() => ({
push: mockPush,
}))

useParamsMock = jest.spyOn(require('react-router-dom'), 'useParams')
useParamsMock.mockImplementation(() => ({
instanceId: 'test-instance-id',
}))
})

afterEach(() => {
useHistoryMock.mockRestore()
useParamsMock.mockRestore()
})

it('should navigate to vector search create index page when start is called', () => {
const { result } = renderHook(() => useStartWizard())

act(() => {
result.current()
})

expect(mockPush).toHaveBeenCalledWith(
Pages.vectorSearchCreateIndex('test-instance-id'),
)
expect(mockPush).toHaveBeenCalledTimes(1)
})

it('should use instanceId from useParams in navigation', () => {
const customInstanceId = 'custom-instance-123'
useParamsMock.mockImplementation(() => ({ instanceId: customInstanceId }))

const { result } = renderHook(() => useStartWizard())

act(() => {
result.current()
})

expect(mockPush).toHaveBeenCalledWith(
Pages.vectorSearchCreateIndex(customInstanceId),
)
})

it('should handle missing instanceId gracefully', () => {
useParamsMock.mockImplementation(() => ({ instanceId: undefined }))

const { result } = renderHook(() => useStartWizard())

act(() => {
result.current()
})

expect(mockPush).toHaveBeenCalledWith(
Pages.vectorSearchCreateIndex(undefined as any),
)
})
})
14 changes: 14 additions & 0 deletions redisinsight/ui/src/pages/vector-search/hooks/useStartWizard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useCallback } from 'react'
import { useHistory, useParams } from 'react-router-dom'
import { Pages } from 'uiSrc/constants'

const useStartWizard = () => {
const history = useHistory()
const { instanceId } = useParams<{ instanceId: string }>()

return useCallback(() => {
history.push(Pages.vectorSearchCreateIndex(instanceId))
}, [history, instanceId])
}

export default useStartWizard
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,21 @@ jest.mock('uiSrc/slices/instances/instances', () => ({

const renderComponent = () => render(<ManageIndexesList />)

const mockedRedisearchListSelector = redisearchListSelector as jest.Mock
const mockedConnectedInstanceSelector = connectedInstanceSelector as jest.Mock

describe('ManageIndexesList', () => {
beforeEach(() => {
cleanup()
jest.clearAllMocks()

// Reset mocks to default state before each test
;(redisearchListSelector as jest.Mock).mockReturnValue({
mockedRedisearchListSelector.mockReturnValue({
data: [],
loading: false,
error: '',
})
;(connectedInstanceSelector as jest.Mock).mockReturnValue({
mockedConnectedInstanceSelector.mockReturnValue({
id: 'test-instance-123',
connectionType: 'STANDALONE',
host: 'localhost',
Expand All @@ -62,7 +65,7 @@ describe('ManageIndexesList', () => {
})

it('should render Loader spinner while fetching data', async () => {
;(redisearchListSelector as jest.Mock).mockReturnValue({
mockedRedisearchListSelector.mockReturnValue({
data: [],
loading: true,
error: '',
Expand All @@ -74,13 +77,28 @@ describe('ManageIndexesList', () => {
expect(loader).toBeInTheDocument()
})

it('should render no indexes message when there are no indexes', async () => {
mockedRedisearchListSelector.mockReturnValue({
data: [],
loading: false,
error: '',
})

renderComponent()

const noIndexesMessage = await screen.getByText(
'No indexes to display yet.',
)
expect(noIndexesMessage).toBeInTheDocument()
})

it('should render indexes boxes when data is available', () => {
const mockIndexes = [
Buffer.from('test-index-1'),
Buffer.from('test-index-2'),
]

;(redisearchListSelector as jest.Mock).mockReturnValue({
mockedRedisearchListSelector.mockReturnValue({
data: mockIndexes,
loading: false,
error: '',
Expand Down Expand Up @@ -109,7 +127,7 @@ describe('ManageIndexesList', () => {
it('should not render indexes boxes when there is no instanceHost', () => {
// Mock connectedInstanceSelector to return no host
// TODO: Potential candidate for a factory function to create mock instances
;(connectedInstanceSelector as jest.Mock).mockReturnValue({
mockedConnectedInstanceSelector.mockReturnValue({
id: 'test-instance-123',
connectionType: 'STANDALONE',
host: null, // No host means no instanceId
Expand All @@ -136,7 +154,7 @@ describe('ManageIndexesList', () => {
it('should not render indexes boxes when redisearch module is not available', () => {
// Mock connectedInstanceSelector to return modules without redisearch
// TODO: Potential candidate for a factory function to create mock instances
;(connectedInstanceSelector as jest.Mock).mockReturnValue({
mockedConnectedInstanceSelector.mockReturnValue({
id: 'test-instance-123',
connectionType: 'STANDALONE',
host: 'localhost',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ import { bufferToString } from 'uiSrc/utils'
import { StyledManageIndexesListAction } from './ManageIndexesList.styles'
import { IndexSection } from './IndexSection'
import { useRedisearchListData } from '../useRedisearchListData'
import NoIndexesMessage from './NoIndexesMessage'

export const ManageIndexesList = () => {
const { data, loading } = useRedisearchListData()
const hasIndexes = !!data?.length

return (
<StyledManageIndexesListAction data-testid="manage-indexes-list">
{loading && <Loader data-testid="manage-indexes-list--loader" />}

{!loading && !hasIndexes && <NoIndexesMessage />}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe worth adding a test for the empty list


{data.map((index) => (
<IndexSection index={index} key={`index-${bufferToString(index)}`} />
))}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react'
import { Button } from 'uiSrc/components/base/forms/buttons'
import { Col } from 'uiSrc/components/base/layout/flex'
import { Text } from 'uiSrc/components/base/text'
import useStartWizard from '../hooks/useStartWizard'

const NoIndexesMessage = () => {
const start = useStartWizard()

return (
<Col gap="m">
<Text size="L">No indexes to display yet.</Text>

<Text>
Complete vector search onboarding to create your first index with sample
data, or create one manually.
</Text>

<Button onClick={start}>Get started</Button>
</Col>
)
}

export default NoIndexesMessage
Original file line number Diff line number Diff line change
@@ -1,49 +1,38 @@
import React from 'react'
import { cleanup, render, screen, userEvent } from 'uiSrc/utils/test-utils'
import { Pages } from 'uiSrc/constants'

import { StartWizardButton } from './StartWizardButton'
import useStartWizard from '../hooks/useStartWizard'

jest.mock('../hooks/useStartWizard', () => ({
__esModule: true,
default: jest.fn(),
}))

const renderComponent = () => render(<StartWizardButton />)
const mockedUseStartWizard = useStartWizard as jest.Mock

describe('StartWizardButton', () => {
beforeEach(() => {
cleanup()
jest.clearAllMocks()
})

it('should navigate to vector search create index page when "Get started" is clicked', async () => {
const mockPush = jest.fn()

const useHistoryMock = jest.spyOn(require('react-router-dom'), 'useHistory')
useHistoryMock.mockImplementation(() => ({
push: mockPush,
}))
it('renders the CTA and calls the start function on click', async () => {
const startMock = jest.fn()
mockedUseStartWizard.mockReturnValue(startMock)

renderComponent()

const getStartedButton = screen.getByText('Get started')
await userEvent.click(getStartedButton)

expect(mockPush).toHaveBeenCalledWith(
Pages.vectorSearchCreateIndex('instanceId'),
)
expect(mockPush).toHaveBeenCalledTimes(1)

useHistoryMock.mockRestore()
})

it('should maintain callback reference stability with useCallback', () => {
const { rerender } = renderComponent()

const firstRenderButton = screen.getByText('Get started')

rerender(<StartWizardButton />)
expect(
screen.getByText(
'Power fast, real-time semantic AI search with vector search.',
),
).toBeInTheDocument()

const secondRenderButton = screen.getByText('Get started')
const btn = screen.getByRole('button', { name: /get started/i })
expect(btn).toBeInTheDocument()

// Both buttons should exist (they're the same element after rerender)
expect(firstRenderButton).toBeInTheDocument()
expect(secondRenderButton).toBeInTheDocument()
await userEvent.click(btn)
expect(startMock).toHaveBeenCalledTimes(1)
})
})
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
import React, { useCallback } from 'react'
import { useHistory, useParams } from 'react-router-dom'
import React from 'react'
import { CallOut } from 'uiSrc/components/base/display/call-out/CallOut'
import { Pages } from 'uiSrc/constants'
import useStartWizard from '../hooks/useStartWizard'

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can probably simplify this component's tests by mocking '../hooks/useStartWizard' as opposed to it's internals ('react-router-dom') since you've already tested them; not mandatory ofc

export const StartWizardButton = () => {
const history = useHistory()
const { instanceId } = useParams<{ instanceId: string }>()

const startCreateIndexWizard = useCallback(() => {
history.push(Pages.vectorSearchCreateIndex(instanceId))
}, [history, instanceId])
const start = useStartWizard()

return (
<CallOut
variant="success"
actions={{
primary: {
label: 'Get started',
onClick: startCreateIndexWizard,
onClick: start,
},
}}
>
Expand Down
Loading