diff --git a/packages/compass-e2e-tests/helpers/commands/create-search-index.ts b/packages/compass-e2e-tests/helpers/commands/create-search-index.ts deleted file mode 100644 index 96af8b0b3a4..00000000000 --- a/packages/compass-e2e-tests/helpers/commands/create-search-index.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { CompassBrowser } from '../compass-browser'; -import * as Selectors from '../selectors'; - -export async function createSearchIndex( - browser: CompassBrowser, - indexName: string, - indexDefinition: string, - screenshotName?: string -): Promise { - // Open modal using dropdown - await browser.clickVisible(Selectors.CreateIndexDropdownButton); - await browser - .$(Selectors.createIndexDropdownAction('search-indexes')) - .waitForDisplayed(); - await browser.clickVisible( - Selectors.createIndexDropdownAction('search-indexes') - ); - - const createModal = await browser.$(Selectors.CreateSearchIndexModal); - await createModal.waitForDisplayed(); - - await browser.setValueVisible(Selectors.CreateSearchIndexName, indexName); - - await browser.setCodemirrorEditorValue( - Selectors.CreateSearchIndexDefinition, - indexDefinition - ); - - if (screenshotName) { - await browser.screenshot(screenshotName); - } - - // Create the index - await browser.clickVisible(Selectors.CreateSearchIndexConfirmButton); - - // Assert that modal goes away - await createModal.waitForDisplayed({ reverse: true }); -} diff --git a/packages/compass-e2e-tests/helpers/commands/drop-search-index.ts b/packages/compass-e2e-tests/helpers/commands/drop-search-index.ts deleted file mode 100644 index 8b225fce619..00000000000 --- a/packages/compass-e2e-tests/helpers/commands/drop-search-index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { CompassBrowser } from '../compass-browser'; -import * as Selectors from '../selectors'; - -export async function dropSearchIndex( - browser: CompassBrowser, - indexName: string, - screenshotName?: string -) { - const indexRowSelector = Selectors.searchIndexRow(indexName); - const indexRow = await browser.$(indexRowSelector); - await indexRow.waitForDisplayed(); - - await browser.hover(indexRowSelector); - await browser.clickVisible(Selectors.searchIndexDropButton(indexName)); - - const dropModal = await browser.$(Selectors.ConfirmationModal); - await dropModal.waitForDisplayed(); - - const confirmInput = await browser.$(Selectors.ConfirmationModalInput); - await confirmInput.waitForDisplayed(); - await confirmInput.setValue(indexName); - - if (screenshotName) { - await browser.screenshot(screenshotName); - } - - await browser.clickVisible(Selectors.ConfirmationModalConfirmButton()); - await dropModal.waitForDisplayed({ reverse: true }); -} diff --git a/packages/compass-e2e-tests/helpers/commands/index.ts b/packages/compass-e2e-tests/helpers/commands/index.ts index f7d73288b05..192cdb2bd0a 100644 --- a/packages/compass-e2e-tests/helpers/commands/index.ts +++ b/packages/compass-e2e-tests/helpers/commands/index.ts @@ -64,5 +64,3 @@ export * from './create-index'; export * from './drop-index'; export * from './hide-index'; export * from './unhide-index'; -export * from './create-search-index'; -export * from './drop-search-index'; diff --git a/packages/compass-e2e-tests/helpers/selectors.ts b/packages/compass-e2e-tests/helpers/selectors.ts index 18dda0a8584..fa30a05ef33 100644 --- a/packages/compass-e2e-tests/helpers/selectors.ts +++ b/packages/compass-e2e-tests/helpers/selectors.ts @@ -941,17 +941,22 @@ export const indexesSegmentedTab = (name: IndexesType) => { // Search Index export const SearchIndexList = '[data-testid="search-indexes"]'; -export const CreateSearchIndexModal = - '[data-testid="create-search-index-modal"]'; -export const CreateSearchIndexName = '[data-testid="name-of-search-index"]'; -export const CreateSearchIndexDefinition = +export const SearchIndexModal = '[data-testid="search-index-modal"]'; +export const SearchIndexName = '[data-testid="name-of-search-index"]'; +export const SearchIndexDefinition = '[data-testid="definition-of-search-index"]'; -export const CreateSearchIndexConfirmButton = - '[data-testid="create-search-index-button"]'; +export const SearchIndexConfirmButton = + '[data-testid="search-index-submit-button"]'; export const searchIndexRow = (name: string) => `[data-testid="search-indexes-row-${name}"]`; +export const searchIndexExpandButton = (name: string) => + `${searchIndexRow(name)} button:first-child`; export const searchIndexDropButton = (name: string) => `${searchIndexRow(name)} [data-testid="search-index-actions-drop-action"]`; +export const searchIndexEditButton = (name: string) => + `${searchIndexRow(name)} [data-testid="search-index-actions-edit-action"]`; +export const searchIndexDetails = (name: string) => + `[data-testid="search-indexes-details-${name}"]`; // Indexes modal export const CreateIndexModal = '[data-testid="create-index-modal"]'; diff --git a/packages/compass-e2e-tests/tests/search-indexes.test.ts b/packages/compass-e2e-tests/tests/search-indexes.test.ts index 1271080fa38..a97051eb59f 100644 --- a/packages/compass-e2e-tests/tests/search-indexes.test.ts +++ b/packages/compass-e2e-tests/tests/search-indexes.test.ts @@ -45,6 +45,92 @@ function getRandomNumber() { return Math.floor(Math.random() * 2 ** 20); } +async function createSearchIndex( + browser: CompassBrowser, + indexName: string, + indexDefinition: string +): Promise { + await browser.clickVisible(Selectors.CreateIndexDropdownButton); + await browser + .$(Selectors.createIndexDropdownAction('search-indexes')) + .waitForDisplayed(); + await browser.clickVisible( + Selectors.createIndexDropdownAction('search-indexes') + ); + + const modal = await browser.$(Selectors.SearchIndexModal); + await modal.waitForDisplayed(); + + await browser.setValueVisible(Selectors.SearchIndexName, indexName); + await browser.setCodemirrorEditorValue( + Selectors.SearchIndexDefinition, + indexDefinition + ); + + await browser.clickVisible(Selectors.SearchIndexConfirmButton); + await modal.waitForDisplayed({ reverse: true }); +} + +async function updateSearchIndex( + browser: CompassBrowser, + indexName: string, + indexDefinition: string +) { + const indexRowSelector = Selectors.searchIndexRow(indexName); + const indexRow = await browser.$(indexRowSelector); + await indexRow.waitForDisplayed(); + + await browser.hover(indexRowSelector); + await browser.clickVisible(Selectors.searchIndexEditButton(indexName)); + + const modal = await browser.$(Selectors.SearchIndexModal); + await modal.waitForDisplayed(); + + await browser.setCodemirrorEditorValue( + Selectors.SearchIndexDefinition, + indexDefinition + ); + + await browser.clickVisible(Selectors.SearchIndexConfirmButton); + await modal.waitForDisplayed({ reverse: true }); +} + +async function dropSearchIndex(browser: CompassBrowser, indexName: string) { + const indexRowSelector = Selectors.searchIndexRow(indexName); + const indexRow = await browser.$(indexRowSelector); + await indexRow.waitForDisplayed(); + + await browser.hover(indexRowSelector); + await browser.clickVisible(Selectors.searchIndexDropButton(indexName)); + + const modal = await browser.$(Selectors.ConfirmationModal); + await modal.waitForDisplayed(); + + const confirmInput = await browser.$(Selectors.ConfirmationModalInput); + await confirmInput.waitForDisplayed(); + await confirmInput.setValue(indexName); + + await browser.clickVisible(Selectors.ConfirmationModalConfirmButton()); + await modal.waitForDisplayed({ reverse: true }); +} + +async function verifyIndexDetails( + browser: CompassBrowser, + indexName: string, + details: string +) { + const indexRowSelector = Selectors.searchIndexRow(indexName); + const indexRow = await browser.$(indexRowSelector); + await indexRow.waitForDisplayed(); + await browser.hover(indexRowSelector); + await browser.clickVisible(Selectors.searchIndexExpandButton(indexName)); + + const text = await browser + .$(Selectors.searchIndexDetails(indexName)) + .getText(); + expect(text.toLowerCase()).to.equal(details.toLowerCase()); +} + describe('Search Indexes', function () { let compass: Compass; let browser: CompassBrowser; @@ -161,24 +247,51 @@ describe('Search Indexes', function () { await browser.clickVisible( Selectors.indexesSegmentedTab('search-indexes') ); - await browser.createSearchIndex(indexName, INDEX_DEFINITION); + await createSearchIndex(browser, indexName, INDEX_DEFINITION); await browser.waitForAnimations(Selectors.SearchIndexList); - const rowSelector = Selectors.searchIndexRow(indexName); - - // View it - await browser.$(rowSelector).waitForDisplayed(); - await browser.waitUntil(async function () { - const text = await browser.$(rowSelector).getText(); - return text.indexOf('PENDING') > -1; - }); + // Verify it was added. + // As we added index definition with no fields and only + // dynamic mapping, the details should display 'Dynamic Mappings' + await verifyIndexDetails(browser, indexName, 'Dynamic Mappings'); // Drop it - await browser.dropSearchIndex(indexName); + await dropSearchIndex(browser, indexName); // todo: check the index status to be either DELETING or the row disappears }); - it('allows users to update and view search indexes'); + it('allows users to update and view search indexes', async function () { + const indexName = `e2e_search_index_${getRandomNumber()}`; + await browser.clickVisible( + Selectors.indexesSegmentedTab('search-indexes') + ); + await createSearchIndex(browser, indexName, INDEX_DEFINITION); + await browser.waitForAnimations(Selectors.SearchIndexList); + + // Verify it was added. + // As we added index definition with no fields and only + // dynamic mapping, the details should display 'Dynamic Mappings' + await verifyIndexDetails(browser, indexName, 'Dynamic Mappings'); + + // Edit it + await updateSearchIndex( + browser, + indexName, + JSON.stringify({ + mappings: { + dynamic: false, + }, + }) + ); + + // Verify its updating/updated. + // As we set the new definition to have no dynamic mappings + // with no fields, the index details should have '[empty]' value. + await verifyIndexDetails(browser, indexName, '[empty]'); + + // Drop it + await dropSearchIndex(browser, indexName); + }); it('runs a search aggregation with index name'); }); } diff --git a/packages/compass-indexes/src/components/indexes/indexes.spec.tsx b/packages/compass-indexes/src/components/indexes/indexes.spec.tsx index 3162a86cece..4fded9c36d2 100644 --- a/packages/compass-indexes/src/components/indexes/indexes.spec.tsx +++ b/packages/compass-indexes/src/components/indexes/indexes.spec.tsx @@ -30,6 +30,9 @@ const renderIndexes = (props: Partial = {}) => { createIndex: { isModalOpen: false, }, + updateIndex: { + isModalOpen: false, + }, }, ...props, }; diff --git a/packages/compass-indexes/src/components/indexes/indexes.tsx b/packages/compass-indexes/src/components/indexes/indexes.tsx index c655a49e678..bf82d85781b 100644 --- a/packages/compass-indexes/src/components/indexes/indexes.tsx +++ b/packages/compass-indexes/src/components/indexes/indexes.tsx @@ -8,7 +8,7 @@ import RegularIndexesTable from '../regular-indexes-table/regular-indexes-table' import SearchIndexesTable from '../search-indexes-table/search-indexes-table'; import { refreshRegularIndexes } from '../../modules/regular-indexes'; import { - openModalForCreation as openAtlasSearchModalForCreation, + showCreateModal as openAtlasSearchModalForCreation, refreshSearchIndexes, } from '../../modules/search-indexes'; import type { State as RegularIndexesState } from '../../modules/regular-indexes'; @@ -16,7 +16,10 @@ import type { State as SearchIndexesState } from '../../modules/search-indexes'; import { SearchIndexesStatuses } from '../../modules/search-indexes'; import type { SearchIndexesStatus } from '../../modules/search-indexes'; import type { RootState } from '../../modules'; -import { CreateSearchIndexModal } from '../search-indexes-modals'; +import { + CreateSearchIndexModal, + UpdateSearchIndexModal, +} from '../search-indexes-modals'; // This constant is used as a trigger to show an insight whenever number of // indexes in a collection is more than what is specified here. @@ -116,6 +119,7 @@ export function Indexes({ )} + ); } diff --git a/packages/compass-indexes/src/components/search-indexes-modals/base-search-index-modal.spec.tsx b/packages/compass-indexes/src/components/search-indexes-modals/base-search-index-modal.spec.tsx new file mode 100644 index 00000000000..2d1a24f7614 --- /dev/null +++ b/packages/compass-indexes/src/components/search-indexes-modals/base-search-index-modal.spec.tsx @@ -0,0 +1,91 @@ +import { expect } from 'chai'; +import { BaseSearchIndexModal } from './base-search-index-modal'; +import sinon from 'sinon'; +import type { SinonSpy } from 'sinon'; + +import { render, screen, cleanup } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import React from 'react'; +import { getCodemirrorEditorValue } from '@mongodb-js/compass-editor'; + +describe('Create Search Index Modal', function () { + let onSubmitSpy: SinonSpy; + let onCloseSpy: SinonSpy; + + beforeEach(function () { + onSubmitSpy = sinon.spy(); + onCloseSpy = sinon.spy(); + + render( + + ); + }); + + afterEach(cleanup); + + describe('default behaviour', function () { + it('uses the initial index name as the default index name', function () { + const inputText: HTMLInputElement = screen.getByTestId( + 'name-of-search-index' + ); + + expect(inputText).to.not.be.null; + expect(inputText?.value).to.equal('default'); + }); + + it('uses a dynamic mapping as the default index definition', function () { + const defaultIndexDef = getCodemirrorEditorValue( + 'definition-of-search-index' + ); + + expect(defaultIndexDef).to.not.be.null; + expect(defaultIndexDef).to.equal('{}'); + }); + }); + + describe('form validation', function () { + it('shows an error when the index name is empty', async function () { + const inputText: HTMLInputElement = screen.getByTestId( + 'name-of-search-index' + ); + + userEvent.clear(inputText); + expect(await screen.findByText('Please enter the name of the index.')).to + .exist; + }); + + it('shows server errors', async function () { + expect(await screen.findByText('Invalid index definition.')).to.exist; + }); + }); + + describe('form behaviour', function () { + it('closes the modal on cancel', function () { + const cancelButton: HTMLButtonElement = screen + .getByText('Cancel') + .closest('button')!; + + userEvent.click(cancelButton); + expect(onCloseSpy).to.have.been.calledOnce; + }); + + it('submits the modal on create search index', function () { + const submitButton: HTMLButtonElement = screen + .getByTestId('search-index-submit-button') + .closest('button')!; + + userEvent.click(submitButton); + expect(onSubmitSpy).to.have.been.calledOnceWithExactly('default', {}); + }); + }); +}); diff --git a/packages/compass-indexes/src/components/search-indexes-modals/base-search-index-modal.tsx b/packages/compass-indexes/src/components/search-indexes-modals/base-search-index-modal.tsx new file mode 100644 index 00000000000..9e13c148b2e --- /dev/null +++ b/packages/compass-indexes/src/components/search-indexes-modals/base-search-index-modal.tsx @@ -0,0 +1,239 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { + Modal, + ModalFooter, + ModalHeader, + ModalBody, + TextInput, + Label, + spacing, + css, + HorizontalRule, + Button, + Link, + Icon, + WarningSummary, + ErrorSummary, + Body, + Banner, +} from '@mongodb-js/compass-components'; +import { CodemirrorMultilineEditor } from '@mongodb-js/compass-editor'; +import _parseShellBSON, { ParseMode } from 'ejson-shell-parser'; +import type { Document } from 'mongodb'; +import { useTrackOnChange } from '@mongodb-js/compass-logging'; + +// Copied from packages/compass-aggregations/src/modules/pipeline-builder/pipeline-parser/utils.ts +function parseShellBSON(source: string): Document[] { + const parsed = _parseShellBSON(source, { mode: ParseMode.Loose }); + if (!parsed || typeof parsed !== 'object') { + // XXX(COMPASS-5689): We've hit the condition in + // https://github.com/mongodb-js/ejson-shell-parser/blob/c9c0145ababae52536ccd2244ac2ad01a4bbdef3/src/index.ts#L36 + throw new Error('The provided index definition is invalid.'); + } + return parsed; +} + +const bodyStyles = css({ + display: 'flex', + flexDirection: 'column', + overflow: 'hidden', + gap: spacing[3], +}); + +const formContainerStyles = css({ + display: 'flex', + flexDirection: 'column', + gap: spacing[3], + overflow: 'auto', + padding: spacing[1], +}); + +const editorStyles = css({ + marginTop: spacing[2], +}); + +const footerStyles = css({ + display: 'flex', + gap: spacing[2], +}); + +type BaseSearchIndexModalProps = { + mode: 'create' | 'update'; + initialIndexName: string; + initialIndexDefinition: string; + isModalOpen: boolean; + isBusy: boolean; + error: string | undefined; + onSubmit: (indexName: string, indexDefinition: Document) => void; + onClose: () => void; +}; + +export const BaseSearchIndexModal: React.FunctionComponent< + BaseSearchIndexModalProps +> = ({ + mode, + initialIndexName, + initialIndexDefinition, + isModalOpen, + isBusy, + error, + onSubmit, + onClose, +}) => { + const [indexName, setIndexName] = useState(initialIndexName); + const [indexDefinition, setIndexDefinition] = useState( + initialIndexDefinition + ); + const [parsingError, setParsingError] = useState( + undefined + ); + + useTrackOnChange( + 'COMPASS-SEARCH-INDEXES-UI', + (track) => { + if (isModalOpen) { + track('Screen', { name: `${mode}_search_index_modal` }); + if (mode === 'create') { + track('Index Create Opened', { + atlas_search: true, + }); + } + } + }, + [isModalOpen, mode], + undefined, + React + ); + + useEffect(() => { + // Reset the name and definition when modal is closed. + if (!isModalOpen) { + setIndexName(initialIndexName); + setIndexDefinition(initialIndexDefinition); + setParsingError(undefined); + } + }, [isModalOpen]); + + useEffect(() => { + setIndexName(initialIndexName); + setIndexDefinition(initialIndexDefinition); + }, [initialIndexName, initialIndexDefinition]); + + const onSearchIndexDefinitionChanged = useCallback( + (newDefinition: string) => { + setParsingError(undefined); + + try { + parseShellBSON(newDefinition); + setIndexDefinition(newDefinition); + } catch (ex) { + setParsingError((ex as Error).message); + } + }, + [setIndexDefinition, setParsingError] + ); + + const onSubmitIndex = useCallback(() => { + if (parsingError) { + setParsingError('The index definition is invalid.'); + return; + } + + const indexDefinitionDoc = parseShellBSON(indexDefinition); + onSubmit(indexName, indexDefinitionDoc); + }, [onSubmit, parsingError, indexName, indexDefinition]); + + return ( + + + +
+ {mode === 'create' && ( + <> +
+ + ) => + setIndexName(evt.target.value) + } + /> +
+ + + )} +
+ +
+ {mode === 'create' && ( + + By default, search indexes will have the following search + configurations. You can refine this later. + + )} + + View Atlas Search tutorials{' '} + + + +
+
+ {parsingError && } + {!parsingError && error && } + {mode === 'update' && ( + + Note: Updating the index may slow down your device temporarily due + to resource usage. Save indexes only with changes to avoid + reindexing. + + )} +
+ + + + +
+ ); +}; diff --git a/packages/compass-indexes/src/components/search-indexes-modals/create-search-index-modal.spec.tsx b/packages/compass-indexes/src/components/search-indexes-modals/create-search-index-modal.spec.tsx index 1b67222fd8d..b3c094f3f7e 100644 --- a/packages/compass-indexes/src/components/search-indexes-modals/create-search-index-modal.spec.tsx +++ b/packages/compass-indexes/src/components/search-indexes-modals/create-search-index-modal.spec.tsx @@ -9,50 +9,51 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { getCodemirrorEditorValue } from '@mongodb-js/compass-editor'; -import { - openModalForCreation, - createIndexFailed, -} from '../../modules/search-indexes'; -import type { IndexesDataService } from '../../stores/store'; +import { showCreateModal } from '../../modules/search-indexes'; import { setupStore } from '../../../test/setup-store'; -describe('Create Search Index Modal', function () { - let store: ReturnType; - let dataProvider: Partial; - - beforeEach(function () { - dataProvider = { - createSearchIndex: sinon.spy(), - }; - - store = setupStore({ namespace: 'test.test' }, dataProvider); - - store.dispatch(openModalForCreation()); - - render( - - - - ); - }); +const renderModal = (createSearchIndexSpy = sinon.spy()) => { + const store = setupStore( + { namespace: 'test.test' }, + { + createSearchIndex: createSearchIndexSpy, + } + ); + store.dispatch(showCreateModal()); + render( + + + + ); + return store; +}; +describe('Create Search Index Modal', function () { afterEach(cleanup); describe('default behaviour', function () { - it('uses "default" as the default index name', function () { + it('renders correct modal title', function () { + renderModal(); + expect( + screen.getByText('Create Search Index', { + selector: 'h1', + }) + ).to.exist; + }); + + it('shows default index name', function () { + renderModal(); const inputText: HTMLInputElement = screen.getByTestId( 'name-of-search-index' ); - - expect(inputText).to.not.be.null; - expect(inputText?.value).to.equal('default'); + expect(inputText!.value).to.equal('default'); }); - it('uses a dynamic mapping as the default index definition', function () { + it('shows default index definition', function () { + renderModal(); const defaultIndexDef = getCodemirrorEditorValue( 'definition-of-search-index' ); - expect(defaultIndexDef).to.not.be.null; expect(defaultIndexDef).to.equal(DEFAULT_INDEX_DEFINITION); }); @@ -60,44 +61,44 @@ describe('Create Search Index Modal', function () { describe('form validation', function () { it('shows an error when the index name is empty', async function () { - const inputText: HTMLInputElement = screen.getByTestId( - 'name-of-search-index' - ); + renderModal(); + const inputText = screen.getByTestId('name-of-search-index'); userEvent.clear(inputText); expect(await screen.findByText('Please enter the name of the index.')).to .exist; }); - it('shows server errors', async function () { - store.dispatch(createIndexFailed('InvalidIndexSpecificationOption')); - expect(store.getState().searchIndexes).to.have.property( + it('shows server errors', function () { + const store = renderModal( + sinon.spy(() => { + throw new Error('Data is invalid'); + }) + ); + store.dispatch(showCreateModal()); + screen.getByTestId('search-index-submit-button').click(); + expect(store.getState().searchIndexes.createIndex).to.have.property( 'error', - 'Invalid index definition.' + 'Data is invalid' ); - expect(await screen.findByText('Invalid index definition.')).to.exist; + expect(screen.getByText('Data is invalid')).to.exist; }); }); describe('form behaviour', function () { it('closes the modal on cancel', function () { - const cancelButton: HTMLButtonElement = screen - .getByText('Cancel') - .closest('button')!; - - userEvent.click(cancelButton); + const store = renderModal(); + screen.getByText('Cancel').click(); expect(store.getState().searchIndexes.createIndex.isModalOpen).to.be .false; }); it('submits the modal on create search index', function () { - const submitButton: HTMLButtonElement = screen - .getByTestId('create-search-index-button') - .closest('button')!; - - userEvent.click(submitButton); - expect(dataProvider.createSearchIndex).to.have.been.calledOnceWithExactly( + const createSearchIndexSpy = sinon.spy(); + renderModal(createSearchIndexSpy); + screen.getByTestId('search-index-submit-button').click(); + expect(createSearchIndexSpy).to.have.been.calledOnceWithExactly( 'test.test', 'default', { mappings: { dynamic: true } } diff --git a/packages/compass-indexes/src/components/search-indexes-modals/create-search-index-modal.tsx b/packages/compass-indexes/src/components/search-indexes-modals/create-search-index-modal.tsx index ad50eae7dec..328f832918a 100644 --- a/packages/compass-indexes/src/components/search-indexes-modals/create-search-index-modal.tsx +++ b/packages/compass-indexes/src/components/search-indexes-modals/create-search-index-modal.tsx @@ -1,199 +1,54 @@ -import React, { useCallback, useState } from 'react'; -import { useTrackOnChange } from '@mongodb-js/compass-logging'; -import { - Modal, - ModalFooter, - ModalHeader, - ModalBody, - TextInput, - Label, - spacing, - css, - HorizontalRule, - Subtitle, - Button, - Link, - Icon, - WarningSummary, - ErrorSummary, -} from '@mongodb-js/compass-components'; -import { closeModal, saveIndex } from '../../modules/search-indexes'; +import React from 'react'; +import { closeCreateModal, createIndex } from '../../modules/search-indexes'; import { connect } from 'react-redux'; -import { CodemirrorMultilineEditor } from '@mongodb-js/compass-editor'; import type { RootState } from '../../modules'; -import _parseShellBSON, { ParseMode } from 'ejson-shell-parser'; import type { Document } from 'mongodb'; - -// Copied from packages/compass-aggregations/src/modules/pipeline-builder/pipeline-parser/utils.ts -function parseShellBSON(source: string): Document[] { - const parsed = _parseShellBSON(source, { mode: ParseMode.Loose }); - if (!parsed || typeof parsed !== 'object') { - // XXX(COMPASS-5689): We've hit the condition in - // https://github.com/mongodb-js/ejson-shell-parser/blob/c9c0145ababae52536ccd2244ac2ad01a4bbdef3/src/index.ts#L36 - throw new Error('The provided index definition is invalid.'); - } - return parsed; -} +import { BaseSearchIndexModal } from './base-search-index-modal'; export const DEFAULT_INDEX_DEFINITION = `{ - "mappings": { - "dynamic": true - } + mappings: { + dynamic: true, + }, }`; -const flexWithGapStyles = css({ - display: 'flex', - flexDirection: 'column', - gap: spacing[3], -}); - -const bodyGapStyles = css({ - marginTop: spacing[3], -}); - -const toolbarStyles = css({ - display: 'flex', - gap: spacing[2], -}); - type CreateSearchIndexModalProps = { isModalOpen: boolean; isBusy: boolean; - error?: string; - saveIndex: (indexName: string, indexDefinition: Document) => void; - closeModal: () => void; + error: string | undefined; + onCreateIndex: (indexName: string, indexDefinition: Document) => void; + onCloseModal: () => void; }; export const CreateSearchIndexModal: React.FunctionComponent< CreateSearchIndexModalProps -> = ({ isModalOpen, isBusy, error, saveIndex, closeModal }) => { - const [indexName, setIndexName] = useState('default'); - const [indexDefinition, setIndexDefinition] = useState( - DEFAULT_INDEX_DEFINITION - ); - const [parsingError, setParsingError] = useState( - undefined - ); - - const onSearchIndexDefinitionChanged = useCallback( - (newDefinition: string) => { - setParsingError(undefined); - - try { - parseShellBSON(newDefinition); - setIndexDefinition(newDefinition); - } catch (ex) { - setParsingError((ex as Error).message); - } - }, - [setIndexDefinition, setParsingError] - ); - - const onSaveIndex = useCallback(() => { - if (parsingError) { - return; - } - - const indexDefinitionDoc = parseShellBSON(indexDefinition); - saveIndex(indexName, indexDefinitionDoc); - }, [saveIndex, parsingError, indexName, indexDefinition]); - - useTrackOnChange( - 'COMPASS-SEARCH-INDEXES-UI', - (track) => { - if (isModalOpen) { - track('Screen', { name: 'create_search_index_modal' }); - track('Index Create Opened', { - atlas_search: true, - }); - } - }, - [isModalOpen], - undefined, - React - ); - +> = ({ isModalOpen, isBusy, error, onCreateIndex, onCloseModal }) => { return ( - - - -
- - ) => - setIndexName(evt.target.value) - } - /> -
- -
- Index Definition -

- By default, search indexes will have the following search - configurations. You can refine this later. -

- - View Atlas Search tutorials{' '} - - - - {parsingError && } - {error && } -
-
- - - - - -
+ ); }; -const mapState = ({ searchIndexes }: RootState) => ({ - isModalOpen: searchIndexes.createIndex.isModalOpen, - isBusy: searchIndexes.createIndex.isBusy, - error: searchIndexes.error, +const mapState = ({ + searchIndexes: { + createIndex: { isBusy, isModalOpen, error }, + }, +}: RootState) => ({ + isModalOpen, + isBusy, + error, }); const mapDispatch = { - closeModal, - saveIndex, + onCloseModal: closeCreateModal, + onCreateIndex: createIndex, }; export default connect(mapState, mapDispatch)(CreateSearchIndexModal); diff --git a/packages/compass-indexes/src/components/search-indexes-modals/index.ts b/packages/compass-indexes/src/components/search-indexes-modals/index.ts index 81595e84444..ecc7e982278 100644 --- a/packages/compass-indexes/src/components/search-indexes-modals/index.ts +++ b/packages/compass-indexes/src/components/search-indexes-modals/index.ts @@ -1 +1,2 @@ export { default as CreateSearchIndexModal } from './create-search-index-modal'; +export { default as UpdateSearchIndexModal } from './update-search-index-modal'; diff --git a/packages/compass-indexes/src/components/search-indexes-modals/update-search-index-modal.spec.tsx b/packages/compass-indexes/src/components/search-indexes-modals/update-search-index-modal.spec.tsx new file mode 100644 index 00000000000..261695c7bdd --- /dev/null +++ b/packages/compass-indexes/src/components/search-indexes-modals/update-search-index-modal.spec.tsx @@ -0,0 +1,115 @@ +import { expect } from 'chai'; +import UpdateSearchIndexModal from './update-search-index-modal'; +import sinon from 'sinon'; +import { Provider } from 'react-redux'; + +import { render, screen, cleanup } from '@testing-library/react'; + +import React from 'react'; +import { getCodemirrorEditorValue } from '@mongodb-js/compass-editor'; +import { + fetchSearchIndexes, + showUpdateModal, +} from '../../modules/search-indexes'; +import { setupStore } from '../../../test/setup-store'; +import { searchIndexes } from '../../../test/fixtures/search-indexes'; + +const renderModal = async ( + indexName: string, + updateSearchIndexSpy = sinon.spy() +) => { + const store = setupStore( + { namespace: 'test.test' }, + { + updateSearchIndex: updateSearchIndexSpy, + getSearchIndexes: () => Promise.resolve(searchIndexes), + } + ); + await store.dispatch(fetchSearchIndexes()); + store.dispatch(showUpdateModal(indexName)); + render( + + + + ); + return store; +}; + +describe('Update Search Index Modal', function () { + afterEach(cleanup); + + describe('default behaviour', function () { + it('renders the modal title', async function () { + await renderModal('cars_index'); + expect(screen.getByText('Edit "cars_index" index')).to.exist; + }); + + it('does not show the input that changes the name of the index', async function () { + await renderModal('default'); + expect(() => screen.getByTestId('name-of-search-index')).to.throw; + }); + + it('uses the provided index definition', async function () { + await renderModal('default'); + const defaultIndexDef = getCodemirrorEditorValue( + 'definition-of-search-index' + ); + expect(JSON.parse(defaultIndexDef)).to.deep.equal({ + mappings: { + dynamic: false, + }, + }); + }); + }); + + describe('form validation', function () { + it('shows server errors', async function () { + const store = await renderModal( + 'default', + sinon.spy(() => { + throw new Error('InvalidIndexSpecificationOption'); + }) + ); + + screen.getByTestId('search-index-submit-button').click(); + + expect(store.getState().searchIndexes.updateIndex).to.have.property( + 'error', + 'Invalid index definition.' + ); + + expect(screen.getByText('Invalid index definition.')).to.exist; + }); + }); + + describe('form behaviour', function () { + it('closes the modal on cancel', async function () { + const store = await renderModal( + 'default', + sinon.spy(() => { + throw new Error('InvalidIndexSpecificationOption'); + }) + ); + screen.getByText('Cancel').click(); + expect(store.getState().searchIndexes.updateIndex.isModalOpen).to.be + .false; + }); + + it('submits the modal on update search index', async function () { + const updateSearchIndexSpy = sinon.spy(() => { + throw new Error('InvalidIndexSpecificationOption'); + }); + await renderModal('default', updateSearchIndexSpy); + screen.getByTestId('search-index-submit-button').click(); + expect(updateSearchIndexSpy).to.have.been.calledOnceWithExactly( + 'test.test', + 'default', + { + mappings: { + dynamic: false, + }, + } + ); + }); + }); +}); diff --git a/packages/compass-indexes/src/components/search-indexes-modals/update-search-index-modal.tsx b/packages/compass-indexes/src/components/search-indexes-modals/update-search-index-modal.tsx new file mode 100644 index 00000000000..56781961ae5 --- /dev/null +++ b/packages/compass-indexes/src/components/search-indexes-modals/update-search-index-modal.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { closeUpdateModal, updateIndex } from '../../modules/search-indexes'; +import { connect } from 'react-redux'; +import type { RootState } from '../../modules'; +import type { Document } from 'mongodb'; +import { BaseSearchIndexModal } from './base-search-index-modal'; + +type UpdateSearchIndexModalProps = { + indexName: string; + indexDefinition: string; + isModalOpen: boolean; + isBusy: boolean; + error: string | undefined; + onUpdateIndex: (indexName: string, indexDefinition: Document) => void; + onCloseModal: () => void; +}; + +export const UpdateSearchIndexModal: React.FunctionComponent< + UpdateSearchIndexModalProps +> = ({ + indexName, + indexDefinition, + isModalOpen, + isBusy, + error, + onUpdateIndex, + onCloseModal, +}) => { + return ( + + ); +}; + +const mapState = ({ + searchIndexes: { + indexes, + updateIndex: { indexName, isBusy, isModalOpen, error }, + }, +}: RootState) => ({ + isModalOpen, + isBusy, + indexName, + indexDefinition: JSON.stringify( + indexes.find((x) => x.name === indexName)?.latestDefinition, + null, + 2 + ), + error, +}); + +const mapDispatch = { + onCloseModal: closeUpdateModal, + onUpdateIndex: updateIndex, +}; + +export default connect(mapState, mapDispatch)(UpdateSearchIndexModal); diff --git a/packages/compass-indexes/src/components/search-indexes-table/search-index-actions.spec.tsx b/packages/compass-indexes/src/components/search-indexes-table/search-index-actions.spec.tsx index ebefc3e34b6..7b44ba8436a 100644 --- a/packages/compass-indexes/src/components/search-indexes-table/search-index-actions.spec.tsx +++ b/packages/compass-indexes/src/components/search-indexes-table/search-index-actions.spec.tsx @@ -10,27 +10,46 @@ import SearchIndexActions from './search-index-actions'; describe('SearchIndexActions Component', function () { let onDropSpy: SinonSpy; + let onEditSpy: SinonSpy; before(cleanup); afterEach(cleanup); beforeEach(function () { onDropSpy = spy(); + onEditSpy = spy(); + render( ); }); it('renders drop button', function () { - const button = screen.getByTestId('search-index-actions-drop-action'); - expect(button).to.exist; - expect(button.getAttribute('aria-label')).to.equal( + const dropButton = screen.getByTestId('search-index-actions-drop-action'); + + expect(dropButton).to.exist; + expect(dropButton.getAttribute('aria-label')).to.equal( 'Drop Index artist_id_index' ); expect(onDropSpy.callCount).to.equal(0); - userEvent.click(button); + userEvent.click(dropButton); expect(onDropSpy.callCount).to.equal(1); + expect(onDropSpy.firstCall.args).to.deep.equal(['artist_id_index']); + }); + + it('renders edit button', function () { + const editButton = screen.getByTestId('search-index-actions-edit-action'); + + expect(editButton).to.exist; + expect(editButton.getAttribute('aria-label')).to.equal( + 'Edit Index artist_id_index' + ); + expect(onEditSpy.callCount).to.equal(0); + userEvent.click(editButton); + expect(onEditSpy.callCount).to.equal(1); + expect(onEditSpy.firstCall.args).to.deep.equal(['artist_id_index']); }); }); diff --git a/packages/compass-indexes/src/components/search-indexes-table/search-index-actions.tsx b/packages/compass-indexes/src/components/search-indexes-table/search-index-actions.tsx index 42afd7e3f87..391ea0a4ea9 100644 --- a/packages/compass-indexes/src/components/search-indexes-table/search-index-actions.tsx +++ b/packages/compass-indexes/src/components/search-indexes-table/search-index-actions.tsx @@ -6,16 +6,23 @@ import type { SearchIndex } from 'mongodb-data-service'; type IndexActionsProps = { index: SearchIndex; onDropIndex: (name: string) => void; + onEditIndex: (name: string) => void; }; -type SearchIndexAction = 'drop'; +type SearchIndexAction = 'drop' | 'edit'; const IndexActions: React.FunctionComponent = ({ index, onDropIndex, + onEditIndex, }) => { const indexActions: GroupedItemAction[] = useMemo(() => { const actions: GroupedItemAction[] = [ + { + action: 'edit', + label: `Edit Index ${index.name}`, + icon: 'Edit', + }, { action: 'drop', label: `Drop Index ${index.name}`, @@ -29,10 +36,12 @@ const IndexActions: React.FunctionComponent = ({ const onAction = useCallback( (action: SearchIndexAction) => { if (action === 'drop') { - void onDropIndex(index.name); + onDropIndex(index.name); + } else if (action === 'edit') { + onEditIndex(index.name); } }, - [onDropIndex, index] + [onDropIndex, onEditIndex, index] ); return ( diff --git a/packages/compass-indexes/src/components/search-indexes-table/search-indexes-table.spec.tsx b/packages/compass-indexes/src/components/search-indexes-table/search-indexes-table.spec.tsx index fa28448a10c..7984fc99e9e 100644 --- a/packages/compass-indexes/src/components/search-indexes-table/search-indexes-table.spec.tsx +++ b/packages/compass-indexes/src/components/search-indexes-table/search-indexes-table.spec.tsx @@ -29,6 +29,7 @@ const renderIndexList = ( readOnly={false} onSortTable={onSortTableSpy} onDropIndex={() => {}} + onEditIndex={() => {}} openCreateModal={openCreateSpy} {...props} /> @@ -163,5 +164,18 @@ describe('SearchIndexesTable Component', function () { dropIndexActions[0].click(); expect(onDropIndexSpy.callCount).to.equal(1); }); + + it('renders edit action and shows modal when clicked', function () { + const onEditIndexSpy = sinon.spy(); + + renderIndexList({ onEditIndex: onEditIndexSpy }); + const editIndexActions = screen.getAllByTestId( + 'search-index-actions-edit-action' + ); + + expect(editIndexActions.length).to.equal(indexes.length); + editIndexActions[0].click(); + expect(onEditIndexSpy.callCount).to.equal(1); + }); }); }); diff --git a/packages/compass-indexes/src/components/search-indexes-table/search-indexes-table.tsx b/packages/compass-indexes/src/components/search-indexes-table/search-indexes-table.tsx index df8e982c380..2a84d0f002e 100644 --- a/packages/compass-indexes/src/components/search-indexes-table/search-indexes-table.tsx +++ b/packages/compass-indexes/src/components/search-indexes-table/search-indexes-table.tsx @@ -18,7 +18,8 @@ import type { SearchSortColumn } from '../../modules/search-indexes'; import { SearchIndexesStatuses, dropSearchIndex, - openModalForCreation, + showCreateModal, + showUpdateModal, } from '../../modules/search-indexes'; import type { SearchIndexesStatus } from '../../modules/search-indexes'; import { sortSearchIndexes } from '../../modules/search-indexes'; @@ -34,6 +35,7 @@ type SearchIndexesTableProps = { readOnly?: boolean; onSortTable: (column: SearchSortColumn, direction: SortDirection) => void; onDropIndex: (name: string) => void; + onEditIndex: (name: string) => void; openCreateModal: () => void; status: SearchIndexesStatus; }; @@ -139,11 +141,13 @@ function SearchIndexDetails({ className={searchIndexDetailsStyles} data-testid={`search-indexes-details-${indexName}`} > - {badges.map((badge) => ( - - {badge.name} - - ))} + {badges.length === 0 + ? '[empty]' + : badges.map((badge) => ( + + {badge.name} + + ))} ); } @@ -156,6 +160,7 @@ export const SearchIndexesTable: React.FunctionComponent< readOnly, onSortTable, openCreateModal, + onEditIndex, status, onDropIndex, }) => { @@ -193,13 +198,20 @@ export const SearchIndexesTable: React.FunctionComponent< ), }, ], + actions: ( + + ), + // TODO(COMPASS-7206): details for the nested row details: ( ), - actions: , }; }); @@ -224,7 +236,8 @@ const mapState = ({ searchIndexes, isWritable }: RootState) => ({ const mapDispatch = { onSortTable: sortSearchIndexes, onDropIndex: dropSearchIndex, - openCreateModal: openModalForCreation, + openCreateModal: showCreateModal, + onEditIndex: showUpdateModal, }; export default connect( diff --git a/packages/compass-indexes/src/modules/search-indexes.spec.ts b/packages/compass-indexes/src/modules/search-indexes.spec.ts index 57ede53cb64..9694477f9f2 100644 --- a/packages/compass-indexes/src/modules/search-indexes.spec.ts +++ b/packages/compass-indexes/src/modules/search-indexes.spec.ts @@ -1,12 +1,15 @@ import { expect } from 'chai'; import { SearchIndexesStatuses, - closeModal, - openModalForCreation, - saveIndex, + closeCreateModal, + showCreateModal, + createIndex, fetchSearchIndexes, sortSearchIndexes, dropSearchIndex, + showUpdateModal, + closeUpdateModal, + updateIndex, } from './search-indexes'; import { setupStore } from '../../test/setup-store'; import { searchIndexes } from '../../test/fixtures/search-indexes'; @@ -25,6 +28,7 @@ describe('search-indexes module', function () { beforeEach(function () { dataProvider = { createSearchIndex: sinon.spy(), + updateSearchIndex: sinon.spy(), }; store = setupStore( @@ -122,7 +126,11 @@ describe('search-indexes module', function () { name: 'default', status: 'READY', queryable: true, - latestDefinition: {}, + latestDefinition: { + mappings: { + dynamic: false, + }, + }, }, ]); expect(state.searchIndexes.sortColumn).to.equal('Name and Fields'); @@ -167,7 +175,11 @@ describe('search-indexes module', function () { name: 'default', status: 'READY', queryable: true, - latestDefinition: {}, + latestDefinition: { + mappings: { + dynamic: false, + }, + }, }, { id: '2', @@ -181,22 +193,36 @@ describe('search-indexes module', function () { }); it('opens the modal for creation', function () { - store.dispatch(openModalForCreation()); + store.dispatch(showCreateModal()); expect(store.getState().searchIndexes.createIndex.isModalOpen).to.be.true; }); - it('closes an open modal', function () { - store.dispatch(openModalForCreation()); - store.dispatch(closeModal()); + it('closes an open modal for creation', function () { + store.dispatch(showCreateModal()); + store.dispatch(closeCreateModal()); expect(store.getState().searchIndexes.createIndex.isModalOpen).to.be.false; }); it('creates the index when data is valid', async function () { - await store.dispatch(saveIndex('indexName', {})); + await store.dispatch(createIndex('indexName', {})); expect(store.getState().searchIndexes.createIndex.isModalOpen).to.be.false; expect(dataProvider.createSearchIndex).to.have.been.calledOnce; }); + it('closes an open modal for update', function () { + store.dispatch(showUpdateModal('indexName')); + store.dispatch(closeUpdateModal()); + + expect(store.getState().searchIndexes.updateIndex.isModalOpen).to.be.false; + }); + + it('updates the index when data is valid', async function () { + await store.dispatch(updateIndex('indexName', {})); + + expect(store.getState().searchIndexes.updateIndex.isModalOpen).to.be.false; + expect(dataProvider.updateSearchIndex).to.have.been.calledOnce; + }); + context('drop search index', function () { let dropSearchIndexStub: sinon.SinonStub; let showConfirmationStub: sinon.SinonStub; diff --git a/packages/compass-indexes/src/modules/search-indexes.ts b/packages/compass-indexes/src/modules/search-indexes.ts index b5972829f23..a433e066be3 100644 --- a/packages/compass-indexes/src/modules/search-indexes.ts +++ b/packages/compass-indexes/src/modules/search-indexes.ts @@ -5,7 +5,7 @@ import { openToast, showConfirmation as showConfirmationModal, } from '@mongodb-js/compass-components'; -import type { Document, MongoServerError } from 'mongodb'; +import type { Document } from 'mongodb'; const ATLAS_SEARCH_SERVER_ERRORS: Record = { InvalidIndexSpecificationOption: 'Invalid index definition.', @@ -52,15 +52,24 @@ export type SearchIndexesStatus = keyof typeof SearchIndexesStatuses; export enum ActionTypes { SetStatus = 'indexes/search-indexes/SetStatus', + SetIsRefreshing = 'indexes/search-indexes/SetIsRefreshing', + SetSearchIndexes = 'indexes/search-indexes/SetSearchIndexes', + SearchIndexesSorted = 'indexes/search-indexes/SearchIndexesSorted', + SetError = 'indexes/search-indexes/SetError', + + // Create Index OpenCreateSearchIndexModal = 'indexes/search-indexes/OpenCreateSearchIndexModal', CreateSearchIndexStarted = 'indexes/search-indexes/CreateSearchIndexStarted', CreateSearchIndexFailed = 'indexes/search-indexes/CreateSearchIndexFailed', CreateSearchIndexSucceeded = 'indexes/search-indexes/CreateSearchIndexSucceed', CreateSearchIndexCancelled = 'indexes/search-indexes/CreateSearchIndexCancelled', - SetIsRefreshing = 'indexes/search-indexes/SetIsRefreshing', - SetSearchIndexes = 'indexes/search-indexes/SetSearchIndexes', - SearchIndexesSorted = 'indexes/search-indexes/SearchIndexesSorted', - SetError = 'indexes/search-indexes/SetError', + + // Update Index + OpenUpdateSearchIndexModal = 'indexes/search-indexes/OpenUpdateSearchIndexModal', + UpdateSearchIndexStarted = 'indexes/search-indexes/UpdateSearchIndexStarted', + UpdateSearchIndexFailed = 'indexes/search-indexes/UpdateSearchIndexFailed', + UpdateSearchIndexSucceeded = 'indexes/search-indexes/UpdateSearchIndexSucceed', + UpdateSearchIndexCancelled = 'indexes/search-indexes/UpdateSearchIndexCancelled', } type SetIsRefreshingAction = { @@ -88,14 +97,45 @@ type CreateSearchIndexCancelledAction = { type: ActionTypes.CreateSearchIndexCancelled; }; +type OpenUpdateSearchIndexModalAction = { + type: ActionTypes.OpenUpdateSearchIndexModal; + indexName: string; +}; + +type UpdateSearchIndexStartedAction = { + type: ActionTypes.UpdateSearchIndexStarted; +}; + +type UpdateSearchIndexFailedAction = { + type: ActionTypes.UpdateSearchIndexFailed; + error: string; +}; + +type UpdateSearchIndexSucceededAction = { + type: ActionTypes.UpdateSearchIndexSucceeded; +}; + +type UpdateSearchIndexCancelledAction = { + type: ActionTypes.UpdateSearchIndexCancelled; +}; + type CreateSearchIndexState = { isModalOpen: boolean; isBusy: boolean; + error?: string; +}; + +type UpdateSearchIndexState = { + isModalOpen: boolean; + isBusy: boolean; + indexName: string; + error?: string; }; export type State = { status: SearchIndexesStatus; createIndex: CreateSearchIndexState; + updateIndex: UpdateSearchIndexState; error?: string; indexes: SearchIndex[]; sortOrder: SortDirection; @@ -131,6 +171,11 @@ export const INITIAL_STATE: State = { isModalOpen: false, isBusy: false, }, + updateIndex: { + isModalOpen: false, + isBusy: false, + indexName: '', + }, error: undefined, indexes: [], sortOrder: 'asc', @@ -174,7 +219,9 @@ export default function reducer( error: action.error, status: SearchIndexesStatuses.ERROR, }; - } else if ( + } + + if ( isAction( action, ActionTypes.OpenCreateSearchIndexModal @@ -182,14 +229,14 @@ export default function reducer( ) { return { ...state, - error: undefined, createIndex: { - ...state.createIndex, isModalOpen: true, isBusy: false, }, }; - } else if ( + } + + if ( isAction( action, ActionTypes.CreateSearchIndexCancelled @@ -197,14 +244,13 @@ export default function reducer( ) { return { ...state, - error: undefined, createIndex: { - ...state.createIndex, isModalOpen: false, isBusy: false, }, }; - } else if ( + } + if ( isAction( action, ActionTypes.CreateSearchIndexStarted @@ -212,13 +258,14 @@ export default function reducer( ) { return { ...state, - error: undefined, createIndex: { ...state.createIndex, isBusy: true, + error: undefined, }, }; - } else if ( + } + if ( isAction( action, ActionTypes.CreateSearchIndexFailed @@ -226,13 +273,14 @@ export default function reducer( ) { return { ...state, - error: action.error, createIndex: { ...state.createIndex, + error: action.error, isBusy: false, }, }; - } else if ( + } + if ( isAction( action, ActionTypes.CreateSearchIndexSucceeded @@ -240,14 +288,89 @@ export default function reducer( ) { return { ...state, - error: undefined, createIndex: { - ...state.createIndex, isModalOpen: false, isBusy: false, }, }; } + if ( + isAction( + action, + ActionTypes.OpenUpdateSearchIndexModal + ) + ) { + return { + ...state, + updateIndex: { + isModalOpen: true, + isBusy: false, + indexName: action.indexName, + }, + }; + } + if ( + isAction( + action, + ActionTypes.UpdateSearchIndexStarted + ) + ) { + return { + ...state, + updateIndex: { + ...state.updateIndex, + error: undefined, + isBusy: true, + }, + }; + } + if ( + isAction( + action, + ActionTypes.UpdateSearchIndexFailed + ) + ) { + return { + ...state, + updateIndex: { + ...state.updateIndex, + error: action.error, + isBusy: false, + }, + }; + } + if ( + isAction( + action, + ActionTypes.UpdateSearchIndexSucceeded + ) + ) { + return { + ...state, + updateIndex: { + ...state.updateIndex, + isBusy: false, + isModalOpen: false, + indexName: '', + }, + }; + } + if ( + isAction( + action, + ActionTypes.UpdateSearchIndexCancelled + ) + ) { + return { + ...state, + updateIndex: { + ...state.updateIndex, + isModalOpen: false, + isBusy: false, + }, + }; + } + return state; } @@ -256,40 +379,39 @@ const setSearchIndexes = (indexes: SearchIndex[]): SetSearchIndexesAction => ({ indexes, }); -export const openModalForCreation = (): OpenCreateSearchIndexModalAction => ({ +export const showCreateModal = (): OpenCreateSearchIndexModalAction => ({ type: ActionTypes.OpenCreateSearchIndexModal, }); -export const closeModal = (): CreateSearchIndexCancelledAction => ({ - type: ActionTypes.CreateSearchIndexCancelled, -}); - -export const createIndexStarted = (): CreateSearchIndexStartedAction => ({ - type: ActionTypes.CreateSearchIndexStarted, +export const showUpdateModal = ( + indexName: string +): OpenUpdateSearchIndexModalAction => ({ + type: ActionTypes.OpenUpdateSearchIndexModal, + indexName, }); -export const createIndexFailed = ( - error: string -): CreateSearchIndexFailedAction => ({ - type: ActionTypes.CreateSearchIndexFailed, - error: ATLAS_SEARCH_SERVER_ERRORS[error] || error, +export const closeCreateModal = (): CreateSearchIndexCancelledAction => ({ + type: ActionTypes.CreateSearchIndexCancelled, }); -export const createIndexSucceeded = (): CreateSearchIndexSucceededAction => ({ - type: ActionTypes.CreateSearchIndexSucceeded, +export const closeUpdateModal = (): UpdateSearchIndexCancelledAction => ({ + type: ActionTypes.UpdateSearchIndexCancelled, }); -export const saveIndex = ( +export const createIndex = ( indexName: string, indexDefinition: Document ): IndexesThunkAction> => { return async function (dispatch, getState) { const { namespace, dataService } = getState(); - dispatch(createIndexStarted()); + dispatch({ type: ActionTypes.CreateSearchIndexStarted }); if (indexName === '') { - dispatch(createIndexFailed('Please enter the name of the index.')); + dispatch({ + type: ActionTypes.CreateSearchIndexFailed, + error: 'Please enter the name of the index.', + }); return; } @@ -300,19 +422,19 @@ export const saveIndex = ( indexDefinition ); } catch (ex) { - dispatch( - createIndexFailed( - (ex as MongoServerError).codeName || (ex as Error).message - ) - ); + const error = (ex as Error).message; + dispatch({ + type: ActionTypes.CreateSearchIndexFailed, + error: ATLAS_SEARCH_SERVER_ERRORS[error] || error, + }); return; } + dispatch({ type: ActionTypes.CreateSearchIndexSucceeded }); track('Index Created', { atlas_search: true, }); - dispatch(createIndexSucceeded()); openToast('search-index-creation-in-progress', { title: `Your index ${indexName} is in progress.`, dismissible: true, @@ -322,6 +444,42 @@ export const saveIndex = ( void dispatch(fetchSearchIndexes()); }; }; + +export const updateIndex = ( + indexName: string, + indexDefinition: Document +): IndexesThunkAction> => { + return async function (dispatch, getState) { + const { namespace, dataService } = getState(); + try { + dispatch({ type: ActionTypes.UpdateSearchIndexStarted }); + await dataService?.updateSearchIndex( + namespace, + indexName, + indexDefinition + ); + dispatch({ type: ActionTypes.UpdateSearchIndexSucceeded }); + track('Index Edited', { + atlas_search: true, + }); + openToast('search-index-update-in-progress', { + title: `Your index ${indexName} is being updated.`, + dismissible: true, + timeout: 5000, + variant: 'success', + }); + void dispatch(fetchSearchIndexes()); + } catch (e) { + const error = (e as Error).message; + dispatch({ + type: ActionTypes.UpdateSearchIndexFailed, + error: ATLAS_SEARCH_SERVER_ERRORS[error] || error, + }); + return; + } + }; +}; + const setError = (error: string | undefined): SetErrorAction => ({ type: ActionTypes.SetError, error, diff --git a/packages/compass-indexes/test/fixtures/search-indexes.ts b/packages/compass-indexes/test/fixtures/search-indexes.ts index 39e6dae8c93..88b703276f3 100644 --- a/packages/compass-indexes/test/fixtures/search-indexes.ts +++ b/packages/compass-indexes/test/fixtures/search-indexes.ts @@ -5,7 +5,11 @@ export const searchIndexes: SearchIndex[] = [ name: 'default', status: 'READY', queryable: true, - latestDefinition: {}, + latestDefinition: { + mappings: { + dynamic: false, + }, + }, }, { id: '2',