Skip to content

RI-7303: Store create index command in Search results #4835

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

Merged
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
@@ -1,43 +1,54 @@
import { renderHook, act } from '@testing-library/react-hooks'
import { useCreateIndex } from './useCreateIndex'
import executeQuery from 'uiSrc/services/executeQuery'
import {
CreateSearchIndexParameters,
SampleDataContent,
SampleDataType,
SearchIndexType,
} from '../types'
import { useCreateIndex } from './useCreateIndex'

const mockLoad = jest.fn()
const mockDispatch = jest.fn()
const mockAddCommands = jest.fn()

jest.mock('uiSrc/services/hooks', () => ({
useLoadData: () => ({
load: mockLoad,
}),
useDispatchWbQuery: () => mockDispatch,
}))

jest.mock('uiSrc/services/workbenchStorage', () => ({
addCommands: (...args: any[]) => mockAddCommands(...args),
}))

jest.mock('uiSrc/utils/index/generateFtCreateCommand', () => ({
generateFtCreateCommand: () => 'FT.CREATE idx:bikes_vss ...',
}))

jest.mock('uiSrc/services/executeQuery', () => ({
__esModule: true,
default: jest.fn(),
}))
const mockExecute = executeQuery as jest.Mock

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

const defaultParams: CreateSearchIndexParameters = {
dataContent: '',
usePresetVectorIndex: true,
presetVectorIndexName: '',
tags: [],
instanceId: 'test-instance-id',
searchIndexType: SearchIndexType.REDIS_QUERY_ENGINE,
dataContent: SampleDataContent.E_COMMERCE_DISCOVERY,
sampleDataType: SampleDataType.PRESET_DATA,
searchIndexType: SearchIndexType.REDIS_QUERY_ENGINE,
usePresetVectorIndex: true,
indexName: 'bikes',
indexFields: [],
}

it('should complete flow successfully', async () => {
mockLoad.mockResolvedValue(undefined)
mockDispatch.mockImplementation((_data, { afterAll }) => afterAll?.())
mockExecute.mockResolvedValue([{ id: '1', databaseId: 'test-instance-id' }])

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

Expand All @@ -46,7 +57,11 @@ describe('useCreateIndex', () => {
})

expect(mockLoad).toHaveBeenCalledWith('test-instance-id', 'bikes')
expect(mockDispatch).toHaveBeenCalled()
expect(mockExecute).toHaveBeenCalledWith(
'test-instance-id',
'FT.CREATE idx:bikes_vss ...',
)
expect(mockAddCommands).toHaveBeenCalled()
expect(result.current.success).toBe(true)
expect(result.current.error).toBeNull()
expect(result.current.loading).toBe(false)
Expand All @@ -62,6 +77,8 @@ describe('useCreateIndex', () => {
expect(result.current.success).toBe(false)
expect(result.current.error?.message).toMatch(/Instance ID is required/)
expect(result.current.loading).toBe(false)
expect(mockLoad).not.toHaveBeenCalled()
expect(mockExecute).not.toHaveBeenCalled()
})

it('should handle failure in data loading', async () => {
Expand All @@ -78,24 +95,23 @@ describe('useCreateIndex', () => {
expect(result.current.success).toBe(false)
expect(result.current.error).toBe(error)
expect(result.current.loading).toBe(false)
expect(mockExecute).not.toHaveBeenCalled()
})

it('should handle dispatch failure', async () => {
it('should handle execution failure', async () => {
mockLoad.mockResolvedValue(undefined)
mockDispatch.mockImplementation((_data, { onFail }) =>
onFail?.(new Error('Dispatch failed')),
)
mockExecute.mockRejectedValue(new Error('Execution failed'))

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

await act(async () => {
await result.current.run(defaultParams)
})

expect(mockDispatch).toHaveBeenCalled()
expect(mockExecute).toHaveBeenCalled()
expect(result.current.success).toBe(false)
expect(result.current.error).toBeInstanceOf(Error)
expect(result.current.error?.message).toBe('Dispatch failed')
expect(result.current.error?.message).toBe('Execution failed')
expect(result.current.loading).toBe(false)
})
})
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { useCallback, useState } from 'react'
import { useLoadData, useDispatchWbQuery } from 'uiSrc/services/hooks'
import { reverse } from 'lodash'
import { useLoadData } from 'uiSrc/services/hooks'
import { addCommands } from 'uiSrc/services/workbenchStorage'
import { generateFtCreateCommand } from 'uiSrc/utils/index/generateFtCreateCommand'
import { CreateSearchIndexParameters, PresetDataType } from '../types'
import executeQuery from 'uiSrc/services/executeQuery'

interface UseCreateIndexResult {
run: (params: CreateSearchIndexParameters) => Promise<void>
Expand All @@ -20,7 +23,6 @@ export const useCreateIndex = (): UseCreateIndexResult => {
const [error, setError] = useState<Error | null>(null)

const { load } = useLoadData()
const dispatchCreateIndex = useDispatchWbQuery()

const run = useCallback(
async ({ instanceId }: CreateSearchIndexParameters) => {
Expand All @@ -40,22 +42,22 @@ export const useCreateIndex = (): UseCreateIndexResult => {
await load(instanceId, collectionName)

// Step 2: Create the search index
await new Promise<void>((resolve, reject) => {
dispatchCreateIndex(generateFtCreateCommand(), {
afterAll: () => {
setSuccess(true)
resolve()
},
onFail: reject,
})
})
const cmd = generateFtCreateCommand()
const data = await executeQuery(instanceId, cmd)

// Step 3: Persist results locally so Vector Search history (CommandsView) shows it
if (Array.isArray(data) && data.length) {
await addCommands(reverse(data))
}

setSuccess(true)
} catch (e) {
setError(e instanceof Error ? e : new Error(String(e)))
} finally {
setLoading(false)
}
},
[load, dispatchCreateIndex],
[load, executeQuery],
)

return {
Expand Down
88 changes: 88 additions & 0 deletions redisinsight/ui/src/services/executeQuery.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { rest } from 'msw'
import { ApiEndpoints } from 'uiSrc/constants'
import { mswServer } from 'uiSrc/mocks/server'
import { getMswURL } from 'uiSrc/utils/test-utils'
import { getUrl } from 'uiSrc/utils'
import { RunQueryMode, ResultsMode } from 'uiSrc/slices/interfaces'
import executeQuery from './executeQuery'

describe('executeQuery', () => {
const instanceId = 'test-instance-id'
const command = 'FT.CREATE idx:bikes_vss ...'

beforeEach(() => {
mswServer.resetHandlers()
jest.clearAllMocks()
})

it.each([null, undefined])(
'returns empty array and does not call API when data is %s',
async (data) => {
const result = await executeQuery(instanceId, data as any)
expect(result).toEqual([])
},
)

it('calls API with correct parameters and returns result', async () => {
const mockResponse = [{ id: '1', databaseId: instanceId }]

mswServer.use(
rest.post(
getMswURL(
getUrl(instanceId, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),
),
async (req, res, ctx) => {
const body = await req.json()
expect(body).toEqual({
commands: [command],
mode: RunQueryMode.ASCII,
resultsMode: ResultsMode.Default,
type: 'SEARCH',
})
return res(ctx.status(200), ctx.json(mockResponse))
},
),
)

const returned = await executeQuery(instanceId, command)
expect(returned).toEqual(mockResponse)
})

it('invokes afterAll callback on success', async () => {
const mockResponse = [{ id: '1', databaseId: instanceId }]

mswServer.use(
rest.post(
getMswURL(
getUrl(instanceId, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),
),
async (_req, res, ctx) => res(ctx.status(200), ctx.json(mockResponse)),
),
)

const afterAll = jest.fn()

await executeQuery(instanceId, command, { afterAll })

expect(afterAll).toHaveBeenCalled()
})

it('invokes onFail and rethrows on error', async () => {
mswServer.use(
rest.post(
getMswURL(
getUrl(instanceId, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),
),
async (_req, res, ctx) => res(ctx.status(500)),
),
)

const onFail = jest.fn()

await expect(
executeQuery(instanceId, command, { onFail }),
).rejects.toThrow()

expect(onFail).toHaveBeenCalled()
})
})
31 changes: 31 additions & 0 deletions redisinsight/ui/src/services/executeQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { RunQueryMode, ResultsMode } from 'uiSrc/slices/interfaces'
import { executeApiCall } from 'uiSrc/pages/vector-search/query/utils'

export interface ExecuteQueryOptions {
afterAll?: () => void
onFail?: (error?: unknown) => void
}

const executeQuery = async (
instanceId: string,
data: string | null | undefined,
options: ExecuteQueryOptions = {},
): Promise<Awaited<ReturnType<typeof executeApiCall>>> => {
if (!data) return [] as unknown as Awaited<ReturnType<typeof executeApiCall>>

try {
const result = await executeApiCall(
instanceId,
[data],
RunQueryMode.ASCII,
ResultsMode.Default,
)
options.afterAll?.()
return result
} catch (e) {
options.onFail?.(e)
throw e
}
}

export default executeQuery
1 change: 0 additions & 1 deletion redisinsight/ui/src/services/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@ export * from './hooks'
export * from './useWebworkers'
export * from './useCabability'
export * from './useStateWithContext'
export * from './useDispatchWbQuery'
export * from './useLoadData'
83 changes: 0 additions & 83 deletions redisinsight/ui/src/services/hooks/useDispatchWbQuery.spec.ts

This file was deleted.

Loading
Loading