diff --git a/redisinsight/ui/src/components/new-index/create-index-step/CreateIndexStepWrapper.spec.tsx b/redisinsight/ui/src/components/new-index/create-index-step/CreateIndexStepWrapper.spec.tsx new file mode 100644 index 0000000000..a84c28a467 --- /dev/null +++ b/redisinsight/ui/src/components/new-index/create-index-step/CreateIndexStepWrapper.spec.tsx @@ -0,0 +1,63 @@ +import React from 'react' +import { cleanup, fireEvent, render, screen } from 'uiSrc/utils/test-utils' +import { CreateIndexStepWrapper } from './CreateIndexStepWrapper' + +const renderComponent = () => render() + +describe('CreateIndexStepWrapper', () => { + beforeEach(() => { + cleanup() + }) + + it('should render', () => { + const { container } = renderComponent() + + expect(container).toBeTruthy() + + // Check if the tabs are rendered + const buildNewIndexTabTrigger = screen.getByText('Build new index') + const usePresetIndexTabTrigger = screen.getByText('Use preset index') + + expect(buildNewIndexTabTrigger).toBeInTheDocument() + expect(usePresetIndexTabTrigger).toBeInTheDocument() + + // Check if the "Use preset index" tab content is selected by default + const usePresetIIndexTabContent = screen.queryByTestId( + 'vector-inde-tabs--use-preset-index-content', + ) + expect(usePresetIIndexTabContent).toBeInTheDocument() + }) + + it('should switch to "Use preset index" tab when clicked', () => { + renderComponent() + + const buildNewIndexTabTrigger = screen.getByText('Use preset index') + fireEvent.click(buildNewIndexTabTrigger) + + // Check if the "Use preset index" tab is rendered + const buildNewIndexTabContent = screen.queryByTestId( + 'vector-inde-tabs--use-preset-index-content', + ) + expect(buildNewIndexTabContent).toBeInTheDocument() + }) + + it('shouldn\'t switch to "Build new index" tab when clicked, since it is disabled', () => { + renderComponent() + + const buildNewIndexTabTriggerLabel = screen.getByText('Build new index') + const buildNewIndexTabTriggerButton = + buildNewIndexTabTriggerLabel.closest('[type="button"]') + + expect(buildNewIndexTabTriggerButton).toHaveAttribute('disabled') + expect(buildNewIndexTabTriggerButton).toHaveAttribute('data-disabled') + + // And when clicked, it should not change the active tab + fireEvent.click(buildNewIndexTabTriggerLabel) + + // Check if the "Use preset index" tab is still active + const usePresetIndexTabContent = screen.queryByTestId( + 'vector-inde-tabs--use-preset-index-content', + ) + expect(usePresetIndexTabContent).toBeInTheDocument() + }) +}) diff --git a/redisinsight/ui/src/components/new-index/create-index-step/CreateIndexStepWrapper.tsx b/redisinsight/ui/src/components/new-index/create-index-step/CreateIndexStepWrapper.tsx new file mode 100644 index 0000000000..3d514b0675 --- /dev/null +++ b/redisinsight/ui/src/components/new-index/create-index-step/CreateIndexStepWrapper.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import { TabsProps } from '@redis-ui/components' +import Tabs, { TabInfo } from 'uiSrc/components/base/layout/tabs' +import { BuildNewIndexTabTrigger } from './build-new-index-tab/BuildNewIndexTabTrigger' + +export enum VectorIndexTab { + BuildNewIndex = 'build-new-index', + UsePresetIndex = 'use-preset-index', +} + +const VECTOR_INDEX_TABS: TabInfo[] = [ + { + value: VectorIndexTab.BuildNewIndex, + label: , + content: null, + disabled: true, + }, + { + value: VectorIndexTab.UsePresetIndex, + label: 'Use preset index', + content: ( +
+ TODO: Add content later +
+ ), + }, +] + +export const CreateIndexStepWrapper = (props: Partial) => { + const { tabs, defaultValue, ...rest } = props + + return ( + + ) +} diff --git a/redisinsight/ui/src/components/new-index/create-index-step/build-new-index-tab/BuildNewIndexTabTrigger.styles.ts b/redisinsight/ui/src/components/new-index/create-index-step/build-new-index-tab/BuildNewIndexTabTrigger.styles.ts new file mode 100644 index 0000000000..5715bf56bf --- /dev/null +++ b/redisinsight/ui/src/components/new-index/create-index-step/build-new-index-tab/BuildNewIndexTabTrigger.styles.ts @@ -0,0 +1,7 @@ +import styled from 'styled-components' + +export const StyledBuildNewIndexTabTrigger = styled.div` + display: flex; + align-items: center; + gap: ${({ theme }) => theme.core.space.space050}; +` diff --git a/redisinsight/ui/src/components/new-index/create-index-step/build-new-index-tab/BuildNewIndexTabTrigger.tsx b/redisinsight/ui/src/components/new-index/create-index-step/build-new-index-tab/BuildNewIndexTabTrigger.tsx new file mode 100644 index 0000000000..4c9eba37e8 --- /dev/null +++ b/redisinsight/ui/src/components/new-index/create-index-step/build-new-index-tab/BuildNewIndexTabTrigger.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import { Badge } from '@redis-ui/components' +import { StyledBuildNewIndexTabTrigger } from './BuildNewIndexTabTrigger.styles' + +export const BuildNewIndexTabTrigger = () => ( + + Build new index + +) diff --git a/redisinsight/ui/src/components/new-index/create-index-step/field-box/FieldBox.spec.tsx b/redisinsight/ui/src/components/new-index/create-index-step/field-box/FieldBox.spec.tsx new file mode 100644 index 0000000000..48aa8d50d8 --- /dev/null +++ b/redisinsight/ui/src/components/new-index/create-index-step/field-box/FieldBox.spec.tsx @@ -0,0 +1,84 @@ +import React from 'react' +import { BoxSelectionGroup } from '@redis-ui/components' + +import { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants' +import { MOCK_VECTOR_SEARCH_BOX } from 'uiSrc/constants/mocks/mock-vector-index-search' +import { cleanup, fireEvent, render, screen } from 'uiSrc/utils/test-utils' + +import { FieldBox, FieldBoxProps } from './FieldBox' +import { VectorSearchBox } from './types' + +const renderFieldBoxComponent = (props?: FieldBoxProps) => { + const defaultProps: FieldBoxProps = { + box: MOCK_VECTOR_SEARCH_BOX, + } + + return render( + + + , + ) +} + +describe('CreateIndexStepWrapper', () => { + beforeEach(() => { + cleanup() + }) + + it('should render', () => { + const props: FieldBoxProps = { + box: { + ...MOCK_VECTOR_SEARCH_BOX, + value: 'id', + label: 'id', + text: 'Unique product identifier', + tag: FieldTypes.TAG, + disabled: false, + }, + } + + const { container } = renderFieldBoxComponent(props) + + expect(container).toBeTruthy() + + // Check if the box is rendered with the correct visual elements + const label = screen.getByText(props.box.label!) + const description = screen.getByText(props.box.text!) + const tag = screen.getByText(props.box.tag.toUpperCase()!) + const checkbox = screen.getByRole('checkbox') + + expect(label).toBeInTheDocument() + expect(description).toBeInTheDocument() + expect(tag).toBeInTheDocument() + expect(checkbox).toBeInTheDocument() + }) + + it('should select the box when clicked', () => { + renderFieldBoxComponent() + + const checkbox = screen.getByRole('checkbox') + expect(checkbox).not.toBeChecked() + + const box = screen.getByTestId(`field-box-${MOCK_VECTOR_SEARCH_BOX.value}`) + fireEvent.click(box) + + expect(checkbox).toBeChecked() + }) + + it('should not select the box when clicked if disabled', () => { + const disabledBox: VectorSearchBox = { + ...MOCK_VECTOR_SEARCH_BOX, + disabled: true, + } + + renderFieldBoxComponent({ box: disabledBox }) + + const checkbox = screen.getByRole('checkbox') + expect(checkbox).not.toBeChecked() + + const box = screen.getByTestId(`field-box-${disabledBox.value}`) + fireEvent.click(box) + + expect(checkbox).not.toBeChecked() + }) +}) diff --git a/redisinsight/ui/src/components/new-index/create-index-step/field-box/FieldBox.styles.ts b/redisinsight/ui/src/components/new-index/create-index-step/field-box/FieldBox.styles.ts new file mode 100644 index 0000000000..5028b0511f --- /dev/null +++ b/redisinsight/ui/src/components/new-index/create-index-step/field-box/FieldBox.styles.ts @@ -0,0 +1,25 @@ +import { BoxSelectionGroup } from '@redis-ui/components' +import styled from 'styled-components' + +export const StyledFieldBox = styled(BoxSelectionGroup.Item.Compose)` + display: flex; + flex-direction: column; + justify-content: space-between; + width: 100%; + padding: ${({ theme }) => theme.core.space.space100}; + gap: ${({ theme }) => theme.components.boxSelectionGroup.defaultItem.gap}; +` + +export const BoxHeader = styled.div` + display: flex; + align-items: center; + justify-content: space-between; +` + +export const BoxHeaderActions = styled.div` + display: flex; + align-items: center; + gap: ${({ theme }) => theme.components.boxSelectionGroup.defaultItem.gap}; +` + +export const BoxContent = styled.div`` diff --git a/redisinsight/ui/src/components/new-index/create-index-step/field-box/FieldBox.tsx b/redisinsight/ui/src/components/new-index/create-index-step/field-box/FieldBox.tsx new file mode 100644 index 0000000000..08c016229e --- /dev/null +++ b/redisinsight/ui/src/components/new-index/create-index-step/field-box/FieldBox.tsx @@ -0,0 +1,53 @@ +import React from 'react' +import { + BoxSelectionGroup, + BoxSelectionGroupItemComposeProps, + Checkbox, +} from '@redis-ui/components' + +import { EditIcon } from 'uiSrc/components/base/icons' +import { IconButton } from 'uiSrc/components/base/forms/buttons/IconButton' +import { Text } from 'uiSrc/components/base/text' + +import { + BoxContent, + BoxHeader, + BoxHeaderActions, + StyledFieldBox, +} from './FieldBox.styles' +import { FieldTag } from './FieldTag' +import { VectorSearchBox } from './types' + +export interface FieldBoxProps extends BoxSelectionGroupItemComposeProps { + box: VectorSearchBox +} + +export const FieldBox = ({ box, ...rest }: FieldBoxProps) => { + const { label, text, tag, disabled } = box + + return ( + + + + {(props) => } + + + + + + + + + + {label} + + + {text && ( + + {text} + + )} + + + ) +} diff --git a/redisinsight/ui/src/components/new-index/create-index-step/field-box/FieldTag.tsx b/redisinsight/ui/src/components/new-index/create-index-step/field-box/FieldTag.tsx new file mode 100644 index 0000000000..b35f48ad8c --- /dev/null +++ b/redisinsight/ui/src/components/new-index/create-index-step/field-box/FieldTag.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import { Badge } from '@redis-ui/components' +import { + FIELD_TYPE_OPTIONS, + FieldTypes, +} from 'uiSrc/pages/browser/components/create-redisearch-index/constants' + +// TODO: Add colors mapping for tags when @redis-ui/components v38.6.0 is released +export const FieldTag = ({ tag }: { tag: FieldTypes }) => { + const tagLabel = FIELD_TYPE_OPTIONS.find( + (option) => option.value === tag, + )?.text + + return tagLabel ? : null +} diff --git a/redisinsight/ui/src/components/new-index/create-index-step/field-box/types.ts b/redisinsight/ui/src/components/new-index/create-index-step/field-box/types.ts new file mode 100644 index 0000000000..db9a8111ad --- /dev/null +++ b/redisinsight/ui/src/components/new-index/create-index-step/field-box/types.ts @@ -0,0 +1,7 @@ +import { BoxSelectionGroupBox } from '@redis-ui/components' +import { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants' + +export interface VectorSearchBox extends BoxSelectionGroupBox { + text: string + tag: FieldTypes +} diff --git a/redisinsight/ui/src/components/new-index/create-index-step/field-boxes-group/FieldBoxesGroup.spec.tsx b/redisinsight/ui/src/components/new-index/create-index-step/field-boxes-group/FieldBoxesGroup.spec.tsx new file mode 100644 index 0000000000..20671839e6 --- /dev/null +++ b/redisinsight/ui/src/components/new-index/create-index-step/field-boxes-group/FieldBoxesGroup.spec.tsx @@ -0,0 +1,55 @@ +import React from 'react' + +import { fireEvent, render, screen } from 'uiSrc/utils/test-utils' +import { MOCK_VECTOR_SEARCH_BOX } from 'uiSrc/constants/mocks/mock-vector-index-search' +import { FieldBoxesGroup, FieldBoxesGroupProps } from './FieldBoxesGroup' + +const renderFieldBoxesGroupComponent = ( + props?: Partial, +) => { + const defaultProps: FieldBoxesGroupProps = { + boxes: [MOCK_VECTOR_SEARCH_BOX], + value: [MOCK_VECTOR_SEARCH_BOX.value], + onChange: jest.fn(), + } + + return render() +} + +describe('FieldBoxesGroup', () => { + it('should render', () => { + const { container } = renderFieldBoxesGroupComponent() + + expect(container).toBeTruthy() + + const fieldBoxesGroup = screen.getByTestId('field-boxes-group') + expect(fieldBoxesGroup).toBeInTheDocument() + + const fieldBoxes = screen.getAllByTestId(/field-box-/) + expect(fieldBoxes).toHaveLength(1) + }) + + it('should call onChange when clicking on a box to select it', () => { + const onChangeMock = jest.fn() + const value: string[] = [] + + renderFieldBoxesGroupComponent({ value, onChange: onChangeMock }) + + const box = screen.getByTestId(`field-box-${MOCK_VECTOR_SEARCH_BOX.value}`) + + fireEvent.click(box) + expect(onChangeMock).toHaveBeenCalledWith([MOCK_VECTOR_SEARCH_BOX.value]) + }) + + it('should call onChange when clicking on a box to deselect it', () => { + const onChangeMock = jest.fn() + const value: string[] = [MOCK_VECTOR_SEARCH_BOX.value] + + renderFieldBoxesGroupComponent({ value, onChange: onChangeMock }) + + const box = screen.getByTestId(`field-box-${MOCK_VECTOR_SEARCH_BOX.value}`) + + fireEvent.click(box) + expect(onChangeMock).toHaveBeenCalledWith([]) + }) +}) diff --git a/redisinsight/ui/src/components/new-index/create-index-step/field-boxes-group/FieldBoxesGroup.styles.ts b/redisinsight/ui/src/components/new-index/create-index-step/field-boxes-group/FieldBoxesGroup.styles.ts new file mode 100644 index 0000000000..b535d1369c --- /dev/null +++ b/redisinsight/ui/src/components/new-index/create-index-step/field-boxes-group/FieldBoxesGroup.styles.ts @@ -0,0 +1,10 @@ +import styled from 'styled-components' +import { MultiBoxSelectionGroup } from '@redis-ui/components' + +export const StyledFieldBoxesGroup = styled(MultiBoxSelectionGroup.Compose)` + display: grid; + grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); + gap: ${({ theme }) => theme.core.space.space150}; + align-items: flex-start; + align-self: stretch; +` diff --git a/redisinsight/ui/src/components/new-index/create-index-step/field-boxes-group/FieldBoxesGroup.tsx b/redisinsight/ui/src/components/new-index/create-index-step/field-boxes-group/FieldBoxesGroup.tsx new file mode 100644 index 0000000000..75305c70cb --- /dev/null +++ b/redisinsight/ui/src/components/new-index/create-index-step/field-boxes-group/FieldBoxesGroup.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import { MultiBoxSelectionGroupProps } from '@redis-ui/components' +import { StyledFieldBoxesGroup } from './FieldBoxesGroup.styles' +import { VectorSearchBox } from '../field-box/types' +import { FieldBox } from '../field-box/FieldBox' + +export interface FieldBoxesGroupProps extends MultiBoxSelectionGroupProps { + boxes: VectorSearchBox[] + value: string[] + onChange: (value: string[] | undefined) => void +} + +export const FieldBoxesGroup = ({ + boxes, + value, + onChange, + ...rest +}: FieldBoxesGroupProps) => ( + + {boxes.map((box) => ( + + ))} + +) diff --git a/redisinsight/ui/src/components/new-index/create-index-step/index.ts b/redisinsight/ui/src/components/new-index/create-index-step/index.ts new file mode 100644 index 0000000000..b342bcebad --- /dev/null +++ b/redisinsight/ui/src/components/new-index/create-index-step/index.ts @@ -0,0 +1,3 @@ +import { CreateIndexStepWrapper } from './CreateIndexStepWrapper' + +export default CreateIndexStepWrapper diff --git a/redisinsight/ui/src/constants/mocks/mock-vector-index-search.ts b/redisinsight/ui/src/constants/mocks/mock-vector-index-search.ts new file mode 100644 index 0000000000..994eff57c9 --- /dev/null +++ b/redisinsight/ui/src/constants/mocks/mock-vector-index-search.ts @@ -0,0 +1,12 @@ +import { VectorSearchBox } from 'uiSrc/components/new-index/create-index-step/field-box/types' +import { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants' + +// TODO: Maybe transform this to a factory function, so it can be reused more easily +// TODO: Maybe make the values more dynamic with faker, so we can test more cases +export const MOCK_VECTOR_SEARCH_BOX: VectorSearchBox = { + value: 'field_mock', + label: 'Field Label Mock', + text: 'Field description mock', + tag: FieldTypes.TEXT, + disabled: false, +}