diff --git a/redisinsight/ui/src/mocks/factories/redisearch/IndexInfo.factory.ts b/redisinsight/ui/src/mocks/factories/redisearch/IndexInfo.factory.ts index dd1fd1dfc5..bc8beefe8d 100644 --- a/redisinsight/ui/src/mocks/factories/redisearch/IndexInfo.factory.ts +++ b/redisinsight/ui/src/mocks/factories/redisearch/IndexInfo.factory.ts @@ -6,7 +6,8 @@ import { IndexAttibuteDto, IndexInfoDto, } from 'apiSrc/modules/browser/redisearch/dto' -import { INDEX_INFO_SEPARATORS } from './IndexInfoTableData.factory' + +export const INDEX_INFO_SEPARATORS: string[] = [',', ';', '|', ':'] // Note: Current data is replica of the sample data, but we can make it more realistic/diverse in the future export const indexInfoFactory = Factory.define(() => ({ @@ -76,21 +77,43 @@ export const indexInfoFactory = Factory.define(() => ({ 'field statistics': indexInfoFieldStatisticsFactory.buildList(3), })) -export const indexInfoAttributeFactory = Factory.define( - () => { - const name = faker.word.noun() +type IndexInfoAttributeFactoryTransientParams = { + includeWeight?: boolean + includeSeparator?: boolean + includeNoIndex?: boolean +} - return { - identifier: `$.${name}`, - attribute: name, - type: faker.helpers.enumValue(FieldTypes).toString(), +export const indexInfoAttributeFactory = Factory.define< + IndexAttibuteDto, + IndexInfoAttributeFactoryTransientParams +>(({ transientParams }) => { + const name = faker.word.noun() + + const { + includeWeight = faker.datatype.boolean(), + includeSeparator = faker.datatype.boolean(), + includeNoIndex = faker.datatype.boolean(), + } = transientParams + + return { + identifier: `$.${name}`, + attribute: name, + type: faker.helpers.enumValue(FieldTypes).toString(), + + // Optional fields + ...(includeWeight && { WEIGHT: faker.number .float({ min: 0.1, max: 10, fractionDigits: 1 }) .toString(), + }), + ...(includeSeparator && { SEPARATOR: faker.helpers.arrayElement(INDEX_INFO_SEPARATORS), - } - }, -) + }), + ...(includeNoIndex && { + NOINDEX: faker.datatype.boolean(), + }), + } +}) export const indexInfoFieldStatisticsFactory = Factory.define(() => { diff --git a/redisinsight/ui/src/mocks/factories/redisearch/IndexInfoTableData.factory.ts b/redisinsight/ui/src/mocks/factories/redisearch/IndexInfoTableData.factory.ts deleted file mode 100644 index f6b4c3e4c0..0000000000 --- a/redisinsight/ui/src/mocks/factories/redisearch/IndexInfoTableData.factory.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Factory } from 'fishery' -import { faker } from '@faker-js/faker' -import { IndexInfoTableData } from 'uiSrc/pages/vector-search/manage-indexes/IndexAttributesList' -import { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants' - -export const INDEX_INFO_SEPARATORS: string[] = [',', ';', '|', ':'] - -type IndexInfoTableDataFactoryTransientParams = { - includeWeight?: boolean - includeSeparator?: boolean -} - -export const indexInfoTableDataFactory = Factory.define< - IndexInfoTableData, - IndexInfoTableDataFactoryTransientParams ->(({ transientParams }) => { - const { - includeWeight = faker.datatype.boolean(), - includeSeparator = faker.datatype.boolean(), - } = transientParams - - return { - attribute: faker.word.sample(), - type: faker.helpers.enumValue(FieldTypes).toString(), - - // Optional fields - ...(includeWeight && { - weight: faker.number - .float({ min: 0.1, max: 10, fractionDigits: 1 }) - .toString(), - }), - ...(includeSeparator && { - separator: faker.helpers.arrayElement(INDEX_INFO_SEPARATORS), - }), - } -}) diff --git a/redisinsight/ui/src/pages/vector-search/manage-indexes/IndexAttributesList.spec.tsx b/redisinsight/ui/src/pages/vector-search/manage-indexes/IndexAttributesList.spec.tsx index f7e8534a73..bd82d5143e 100644 --- a/redisinsight/ui/src/pages/vector-search/manage-indexes/IndexAttributesList.spec.tsx +++ b/redisinsight/ui/src/pages/vector-search/manage-indexes/IndexAttributesList.spec.tsx @@ -1,6 +1,9 @@ import React from 'react' import { cleanup, render, screen } from 'uiSrc/utils/test-utils' -import { indexInfoTableDataFactory } from 'uiSrc/mocks/factories/redisearch/IndexInfoTableData.factory' +import { + indexInfoAttributeFactory, + indexInfoFactory, +} from 'uiSrc/mocks/factories/redisearch/IndexInfo.factory' import { IndexAttributesList, IndexAttributesListProps, @@ -8,7 +11,7 @@ import { const renderComponent = (props?: Partial) => { const defaultProps: IndexAttributesListProps = { - data: indexInfoTableDataFactory.buildList(3), + indexInfo: indexInfoFactory.build(), } return render() @@ -21,12 +24,41 @@ describe('IndexAttributesList', () => { it('should render', () => { const props: IndexAttributesListProps = { - data: [ - indexInfoTableDataFactory.build( - {}, - { transient: { includeWeight: true, includeSeparator: true } }, - ), - ], + indexInfo: indexInfoFactory.build(), + } + + const { container } = renderComponent(props) + expect(container).toBeTruthy() + + const list = screen.getByTestId('index-attributes-list') + expect(list).toBeInTheDocument() + + const table = screen.getByTestId('index-attributes-list--table') + const summaryInfo = screen.getByTestId( + 'index-attributes-list--summary-info', + ) + + expect(table).toBeInTheDocument() + expect(summaryInfo).toBeInTheDocument() + }) + + it('should render loader when index info is not provided', () => { + renderComponent({ indexInfo: undefined }) + + const loader = screen.getByTestId('index-attributes-list--loader') + expect(loader).toBeInTheDocument() + }) + + it('should render index attributes in the table', () => { + const mockIndexAttribute = indexInfoAttributeFactory.build( + {}, + { transient: { includeWeight: true, includeNoIndex: true } }, + ) + + const props: IndexAttributesListProps = { + indexInfo: indexInfoFactory.build({ + attributes: [mockIndexAttribute], + }), } const { container } = renderComponent(props) @@ -36,14 +68,67 @@ describe('IndexAttributesList', () => { expect(list).toBeInTheDocument() // Verify data is rendered correctly - const attribute = screen.getByText(props.data[0].attribute) - const type = screen.getByText(props.data[0].type) - const weight = screen.getByText(props.data[0].weight!) - const separator = screen.getByText(props.data[0].separator!) + const identifier = screen.getByText(mockIndexAttribute.identifier) + const attribute = screen.getByText(mockIndexAttribute.attribute) + const type = screen.getByText(mockIndexAttribute.type) + const weight = screen.getByText(mockIndexAttribute.WEIGHT!) + const noIndex = screen.getByTestId('index-attributes-list--noindex-icon') + expect(identifier).toBeInTheDocument() expect(attribute).toBeInTheDocument() expect(type).toBeInTheDocument() expect(weight).toBeInTheDocument() - expect(separator).toBeInTheDocument() + expect(noIndex).toBeInTheDocument() + expect(noIndex).toHaveAttribute( + 'data-attribute', + mockIndexAttribute.NOINDEX?.toString(), + ) + }) + + it('should display index summary info', () => { + const mockIndexInfo = indexInfoFactory.build() + + const props: IndexAttributesListProps = { + indexInfo: mockIndexInfo, + } + + renderComponent(props) + + const summaryInfo = screen.getByTestId( + 'index-attributes-list--summary-info', + ) + expect(summaryInfo).toBeInTheDocument() + + // Verify Number of documents + const numberOfDocumentLabel = screen.getByText(/Number of docs:/) + const numberOfDocumentValue = screen.getByText( + new RegExp(mockIndexInfo.num_docs), + ) + expect(numberOfDocumentLabel).toBeInTheDocument() + expect(numberOfDocumentValue).toBeInTheDocument() + + // Verify Max document ID + const maxDocumentIdLabel = screen.getByText(/max/) + const maxDocumentIdValue = screen.getByText( + new RegExp(mockIndexInfo.max_doc_id!), + ) + expect(maxDocumentIdLabel).toBeInTheDocument() + expect(maxDocumentIdValue).toBeInTheDocument() + + // Verify Number of records + const numberOfRecordsLabel = screen.getByText(/Number of records:/) + const numberOfRecordsValue = screen.getByText( + new RegExp(mockIndexInfo.num_records!), + ) + expect(numberOfRecordsLabel).toBeInTheDocument() + expect(numberOfRecordsValue).toBeInTheDocument() + + // Verify Number of terms + const numberOfTermsLabel = screen.getByText(/Number of terms:/) + const numberOfTermsValue = screen.getByText( + new RegExp(mockIndexInfo.num_terms!), + ) + expect(numberOfTermsLabel).toBeInTheDocument() + expect(numberOfTermsValue).toBeInTheDocument() }) }) diff --git a/redisinsight/ui/src/pages/vector-search/manage-indexes/IndexAttributesList.styles.ts b/redisinsight/ui/src/pages/vector-search/manage-indexes/IndexAttributesList.styles.ts index 877ba7f344..f114e15dd7 100644 --- a/redisinsight/ui/src/pages/vector-search/manage-indexes/IndexAttributesList.styles.ts +++ b/redisinsight/ui/src/pages/vector-search/manage-indexes/IndexAttributesList.styles.ts @@ -1,9 +1,16 @@ import styled from 'styled-components' export const StyledIndexAttributesList = styled.div` - > *:first-child { - box-shadow: none !important; - border-radius: 0; - margin: -30px 0; - } + display: flex; + gap: ${({ theme }) => theme.core.space.space150}; + flex-direction: column; +` + +export const StyledIndexAttributesTable = styled.div` + // Drawer width (60rem) minus its padding (2 * 3.2rem), we don't have them as variables in Redis UI + width: calc(60rem - 2 * 3.2rem); +` + +export const StyledIndexSummaryInfo = styled.div` + font-size: ${({ theme }) => theme.core.font.fontSize.s12}; ` diff --git a/redisinsight/ui/src/pages/vector-search/manage-indexes/IndexAttributesList.tsx b/redisinsight/ui/src/pages/vector-search/manage-indexes/IndexAttributesList.tsx index f0fb14afbb..9b0b2d4fcc 100644 --- a/redisinsight/ui/src/pages/vector-search/manage-indexes/IndexAttributesList.tsx +++ b/redisinsight/ui/src/pages/vector-search/manage-indexes/IndexAttributesList.tsx @@ -1,15 +1,28 @@ import React from 'react' import { ColumnDefinition, Table } from 'uiSrc/components/base/layout/table' -import { StyledIndexAttributesList } from './IndexAttributesList.styles' +import { RiIcon } from 'uiSrc/components/base/icons' +import { Loader } from 'uiSrc/components/base/display' +import { IndexInfoDto } from 'apiSrc/modules/browser/redisearch/dto' +import { + StyledIndexAttributesList, + StyledIndexAttributesTable, + StyledIndexSummaryInfo, +} from './IndexAttributesList.styles' export interface IndexInfoTableData { + identifier: string attribute: string type: string weight?: string - separator?: string + noindex?: boolean } const tableColumns: ColumnDefinition[] = [ + { + header: 'Identifier', + id: 'identifier', + accessorKey: 'identifier', + }, { header: 'Attribute', id: 'attribute', @@ -28,20 +41,59 @@ const tableColumns: ColumnDefinition[] = [ enableSorting: false, }, { - header: 'Separator', - id: 'separator', - accessorKey: 'separator', + header: 'Noindex', + id: 'noindex', + accessorKey: 'noindex', enableSorting: false, + cell: ({ row }) => ( + + ), }, ] export interface IndexAttributesListProps { - data: IndexInfoTableData[] + indexInfo: IndexInfoDto | undefined +} + +export const IndexAttributesList = ({ + indexInfo, +}: IndexAttributesListProps) => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { num_docs, max_doc_id, num_records, num_terms } = indexInfo || {} + + if (!indexInfo) { + return + } + + return ( + + + + + + +

+ Number of docs: {num_docs} (max {max_doc_id}) | Number of records:{' '} + {num_records} | Number of terms: {num_terms} +

+
+ + ) } -export const IndexAttributesList = ({ data }: IndexAttributesListProps) => ( - // @ts-expect-error - styled-components typing issue: The TypeScript definitions for styled-components - -
- -) +const parseIndexAttributes = (indexInfo: IndexInfoDto): IndexInfoTableData[] => + indexInfo.attributes.map((field) => ({ + identifier: field.identifier, + attribute: field.attribute, + type: field.type, + weight: field.WEIGHT, + noindex: field.NOINDEX ?? true, + })) diff --git a/redisinsight/ui/src/pages/vector-search/manage-indexes/IndexSection.spec.tsx b/redisinsight/ui/src/pages/vector-search/manage-indexes/IndexSection.spec.tsx index cd2f338805..a0e7024ae3 100644 --- a/redisinsight/ui/src/pages/vector-search/manage-indexes/IndexSection.spec.tsx +++ b/redisinsight/ui/src/pages/vector-search/manage-indexes/IndexSection.spec.tsx @@ -21,7 +21,10 @@ import notificationsReducer from 'uiSrc/slices/app/notifications' import appInfoReducer from 'uiSrc/slices/app/info' import redisearchReducer from 'uiSrc/slices/browser/redisearch' import instancesReducer from 'uiSrc/slices/instances/instances' -import { indexInfoFactory } from 'uiSrc/mocks/factories/redisearch/IndexInfo.factory' +import { + indexInfoAttributeFactory, + indexInfoFactory, +} from 'uiSrc/mocks/factories/redisearch/IndexInfo.factory' import { IndexInfoDto } from 'apiSrc/modules/browser/redisearch/dto' import { IndexSection, IndexSectionProps } from './IndexSection' @@ -165,7 +168,13 @@ describe('IndexSection', () => { }) it('should display index attributes when expanded', async () => { - const mockIndexInfo = indexInfoFactory.build() + const mockIndexAttribute = indexInfoAttributeFactory.build( + {}, + { transient: { includeWeight: true, includeNoIndex: true } }, + ) + const mockIndexInfo = indexInfoFactory.build({ + attributes: [mockIndexAttribute], + }) const props: IndexSectionProps = { index: mockIndexInfo.index_name, } @@ -188,15 +197,17 @@ describe('IndexSection', () => { await userEvent.click(indexName) // Verify the index attributes are displayed + const identifier = await screen.findByText('Identifier') const attribute = await screen.findByText('Attribute') const type = await screen.findByText('Type') const weight = await screen.findByText('Weight') - const separator = await screen.findByText('Separator') + const noindex = await screen.findByText('Noindex') + expect(identifier).toBeInTheDocument() expect(attribute).toBeInTheDocument() expect(type).toBeInTheDocument() expect(weight).toBeInTheDocument() - expect(separator).toBeInTheDocument() + expect(noindex).toBeInTheDocument() // Verify that data rows are rendered const regularRows = container.querySelectorAll( @@ -205,16 +216,21 @@ describe('IndexSection', () => { expect(regularRows.length).toBe(mockIndexInfo.attributes.length) // Verify their values as well - const mockAttribute = mockIndexInfo.attributes[0] - const attributeValue = await screen.findByText(mockAttribute.attribute) - const typeValue = await screen.findAllByText(mockAttribute.type) - const weightValue = await screen.findAllByText(mockAttribute.WEIGHT!) - const separatorValue = await screen.findAllByText(mockAttribute.SEPARATOR!) + const identifierValue = await screen.findByText( + mockIndexAttribute.identifier, + ) + const attributeValue = await screen.findByText(mockIndexAttribute.attribute) + const typeValue = await screen.findAllByText(mockIndexAttribute.type) + const weightValue = await screen.findAllByText(mockIndexAttribute.WEIGHT!) + const noIndexValue = await screen.findAllByTestId( + 'index-attributes-list--noindex-icon', + ) + expect(identifierValue).toBeInTheDocument() expect(attributeValue).toBeInTheDocument() expect(typeValue[0]).toBeInTheDocument() expect(weightValue[0]).toBeInTheDocument() - expect(separatorValue[0]).toBeInTheDocument() + expect(noIndexValue[0]).toBeInTheDocument() }) describe('delete index', () => { diff --git a/redisinsight/ui/src/pages/vector-search/manage-indexes/IndexSection.tsx b/redisinsight/ui/src/pages/vector-search/manage-indexes/IndexSection.tsx index 276127444d..ab8f667158 100644 --- a/redisinsight/ui/src/pages/vector-search/manage-indexes/IndexSection.tsx +++ b/redisinsight/ui/src/pages/vector-search/manage-indexes/IndexSection.tsx @@ -14,7 +14,7 @@ import { IndexInfoDto, IndexDeleteRequestBodyDto, } from 'apiSrc/modules/browser/redisearch/dto' -import { IndexAttributesList, IndexInfoTableData } from './IndexAttributesList' +import { IndexAttributesList } from './IndexAttributesList' export interface IndexSectionProps extends Omit { index: RedisString @@ -25,7 +25,7 @@ export const IndexSection = ({ index, ...rest }: IndexSectionProps) => { const indexName = bufferToString(index) const { id: instanceId } = useSelector(connectedInstanceSelector) - const [tableData, setTableData] = useState([]) + const [indexInfo, setIndexInfo] = useState() const [indexSummaryInfo, setIndexSummaryInfo] = useState< CategoryValueListItem[] >(parseIndexSummaryInfo({} as IndexInfoDto)) @@ -35,7 +35,7 @@ export const IndexSection = ({ index, ...rest }: IndexSectionProps) => { fetchRedisearchInfoAction(indexName, (data) => { const indexInfo = data as unknown as IndexInfoDto - setTableData(parseIndexAttributes(indexInfo)) + setIndexInfo(indexInfo) setIndexSummaryInfo(parseIndexSummaryInfo(indexInfo)) }), ) @@ -69,7 +69,7 @@ export const IndexSection = ({ index, ...rest }: IndexSectionProps) => {
} - content={} + content={} // TODO: Add FieldTag component to list the types of the different fields label={formatLongName(indexName)} defaultOpen={false} @@ -106,11 +106,3 @@ const parseIndexSummaryInfo = ( // key: 'date', // }, ] - -const parseIndexAttributes = (indexInfo: IndexInfoDto): IndexInfoTableData[] => - indexInfo.attributes.map((field) => ({ - attribute: field.attribute, - type: field.type, - weight: field.WEIGHT, - separator: field.SEPARATOR, - }))