diff --git a/packages/compass-indexes/src/components/create-index-actions/create-index-actions.spec.jsx b/packages/compass-indexes/src/components/create-index-actions/create-index-actions.spec.jsx index ba27d6421f3..f76599042c2 100644 --- a/packages/compass-indexes/src/components/create-index-actions/create-index-actions.spec.jsx +++ b/packages/compass-indexes/src/components/create-index-actions/create-index-actions.spec.jsx @@ -5,15 +5,12 @@ import sinon from 'sinon'; import { render, screen, - cleanup, - fireEvent, + userEvent, within, } from '@mongodb-js/testing-library-compass'; import CreateIndexActions from '../create-index-actions'; -const noop = () => {}; - describe('CreateIndexActions Component', function () { let clearErrorSpy; let onCreateIndexClickSpy; @@ -29,8 +26,6 @@ describe('CreateIndexActions Component', function () { clearErrorSpy = null; onCreateIndexClickSpy = null; closeCreateIndexModalSpy = null; - - cleanup(); }); it('renders a cancel button', function () { @@ -38,7 +33,6 @@ describe('CreateIndexActions Component', function () { @@ -54,14 +48,13 @@ describe('CreateIndexActions Component', function () { ); const button = screen.getByTestId('create-index-actions-cancel-button'); - fireEvent.click(button); + userEvent.click(button); expect(closeCreateIndexModalSpy).to.have.been.calledOnce; }); }); @@ -72,7 +65,6 @@ describe('CreateIndexActions Component', function () { @@ -81,7 +73,7 @@ describe('CreateIndexActions Component', function () { const button = screen.getByTestId( 'create-index-actions-create-index-button' ); - fireEvent.click(button); + userEvent.click(button); expect(onCreateIndexClickSpy).to.have.been.calledOnce; }); }); @@ -91,7 +83,6 @@ describe('CreateIndexActions Component', function () { @@ -109,7 +100,6 @@ describe('CreateIndexActions Component', function () { @@ -126,7 +116,6 @@ describe('CreateIndexActions Component', function () { @@ -137,25 +126,9 @@ describe('CreateIndexActions Component', function () { ); const closeIcon = within(errorBanner).getByLabelText('X Icon'); - fireEvent.click(closeIcon); + userEvent.click(closeIcon); expect(clearErrorSpy).to.have.been.calledOnce; }); - - it('does not render in progress banner', function () { - render( - - ); - - const inProgressBanner = screen.queryByTestId( - 'create-index-actions-in-progress-banner-wrapper' - ); - expect(inProgressBanner).to.not.exist; - }); }); context('without error', function () { @@ -164,7 +137,6 @@ describe('CreateIndexActions Component', function () { @@ -175,42 +147,5 @@ describe('CreateIndexActions Component', function () { ); expect(errorBanner).to.not.exist; }); - - context('when in progress', function () { - beforeEach(function () { - render( - - ); - }); - - afterEach(cleanup); - - it('renders in progress banner', function () { - const inProgressBanner = screen.getByTestId( - 'create-index-actions-in-progress-banner-wrapper' - ); - expect(inProgressBanner).to.contain.text('Index creation in progress'); - }); - - it('hides the create index button', function () { - const onCreateIndexClickButton = screen.queryByTestId( - 'create-index-actions-create-index-button' - ); - expect(onCreateIndexClickButton).to.not.exist; - }); - - it('renames the cancel button to close', function () { - const cancelButton = screen.getByTestId( - 'create-index-actions-cancel-button' - ); - expect(cancelButton.textContent).to.be.equal('Close'); - }); - }); }); }); diff --git a/packages/compass-indexes/src/components/create-index-actions/create-index-actions.tsx b/packages/compass-indexes/src/components/create-index-actions/create-index-actions.tsx index cc1f9ab9991..ab6aab6d67f 100644 --- a/packages/compass-indexes/src/components/create-index-actions/create-index-actions.tsx +++ b/packages/compass-indexes/src/components/create-index-actions/create-index-actions.tsx @@ -24,72 +24,46 @@ const createIndexButtonStyles = css({ */ function CreateIndexActions({ error, - inProgress, onErrorBannerCloseClick, onCreateIndexClick, onCancelCreateIndexClick, }: { error: string | null; - inProgress: boolean; onErrorBannerCloseClick: () => void; onCreateIndexClick: () => void; onCancelCreateIndexClick: () => void; }) { - const renderError = () => { - if (!error) { - return; - } - - return ( -
- - {error} - -
- ); - }; - - const renderInProgress = () => { - if (error || !inProgress) { - return; - } - - return ( -
- - Index creation in progress. The dialog can be closed. - -
- ); - }; - return (
- {renderError()} - {renderInProgress()} + {error && ( +
+ + {error} + +
+ )} + - {!inProgress && ( - - )}
); } diff --git a/packages/compass-indexes/src/components/create-index-modal/create-index-modal.tsx b/packages/compass-indexes/src/components/create-index-modal/create-index-modal.tsx index 189c6988968..2f4e744bc66 100644 --- a/packages/compass-indexes/src/components/create-index-modal/create-index-modal.tsx +++ b/packages/compass-indexes/src/components/create-index-modal/create-index-modal.tsx @@ -12,7 +12,7 @@ import { fieldTypeUpdated, updateFieldName, errorCleared, - createIndex, + createIndexFormSubmitted, createIndexClosed, } from '../../modules/create-index'; import { CreateIndexForm } from '../create-index-form/create-index-form'; @@ -28,7 +28,6 @@ type CreateIndexModalProps = React.ComponentProps & { isVisible: boolean; namespace: string; error: string | null; - inProgress: boolean; onErrorBannerCloseClick: () => void; onCreateIndexClick: () => void; onCancelCreateIndexClick: () => void; @@ -38,7 +37,6 @@ function CreateIndexModal({ isVisible, namespace, error, - inProgress, onErrorBannerCloseClick, onCreateIndexClick, onCancelCreateIndexClick, @@ -88,7 +86,6 @@ function CreateIndexModal({ @@ -98,10 +95,9 @@ function CreateIndexModal({ } const mapState = ({ namespace, serverVersion, createIndex }: RootState) => { - const { fields, inProgress, error, isVisible } = createIndex; + const { fields, error, isVisible } = createIndex; return { fields, - inProgress, error, isVisible, namespace, @@ -111,7 +107,7 @@ const mapState = ({ namespace, serverVersion, createIndex }: RootState) => { const mapDispatch = { onErrorBannerCloseClick: errorCleared, - onCreateIndexClick: createIndex, + onCreateIndexClick: createIndexFormSubmitted, onCancelCreateIndexClick: createIndexClosed, onAddFieldClick: fieldAdded, onRemoveFieldClick: fieldRemoved, diff --git a/packages/compass-indexes/src/modules/create-index.spec.ts b/packages/compass-indexes/src/modules/create-index.spec.ts index d96ee47e06a..dd46621441b 100644 --- a/packages/compass-indexes/src/modules/create-index.spec.ts +++ b/packages/compass-indexes/src/modules/create-index.spec.ts @@ -1,10 +1,9 @@ import { expect } from 'chai'; -import sinon from 'sinon'; import { setupStore } from '../../test/setup-store'; import { - createIndex, + createIndexFormSubmitted, updateFieldName, fieldAdded, fieldRemoved, @@ -14,7 +13,6 @@ import { createIndexOpened, createIndexClosed, errorCleared, - INITIAL_STATE, } from './create-index'; import type { IndexesStore } from '../stores/store'; @@ -24,13 +22,13 @@ describe('create-index module', function () { store = setupStore(); }); - describe('#createIndex', function () { + describe('#createIndexFormSubmitted', function () { beforeEach(function () { store.dispatch(updateFieldName(0, 'foo')); store.dispatch(fieldTypeUpdated(0, 'text')); }); - it('validates field name & type', async function () { + it('validates field name & type', function () { Object.assign(store.getState(), { createIndex: { ...store.getState().createIndex, @@ -42,14 +40,14 @@ describe('create-index module', function () { ], }, }); - await store.dispatch(createIndex()); + store.dispatch(createIndexFormSubmitted()); expect(store.getState().createIndex.error).to.equal( 'You must select a field name and type' ); }); - it('validates collation', async function () { + it('validates collation', function () { Object.assign(store.getState(), { createIndex: { ...store.getState().createIndex, @@ -63,14 +61,14 @@ describe('create-index module', function () { }, }, }); - await store.dispatch(createIndex()); + store.dispatch(createIndexFormSubmitted()); expect(store.getState().createIndex.error).to.equal( 'You must provide a valid collation object' ); }); - it('validates TTL', async function () { + it('validates TTL', function () { Object.assign(store.getState(), { createIndex: { ...store.getState().createIndex, @@ -84,14 +82,14 @@ describe('create-index module', function () { }, }, }); - await store.dispatch(createIndex()); + store.dispatch(createIndexFormSubmitted()); expect(store.getState().createIndex.error).to.equal( 'Bad TTL: "not a ttl"' ); }); - it('validates wildcard projection', async function () { + it('validates wildcard projection', function () { Object.assign(store.getState(), { createIndex: { ...store.getState().createIndex, @@ -105,14 +103,14 @@ describe('create-index module', function () { }, }, }); - await store.dispatch(createIndex()); + store.dispatch(createIndexFormSubmitted()); expect(store.getState().createIndex.error).to.equal( 'Bad WildcardProjection: SyntaxError: Unexpected token \'o\', "not a wildc"... is not valid JSON' ); }); - it('validates columnstore projection', async function () { + it('validates columnstore projection', function () { Object.assign(store.getState(), { createIndex: { ...store.getState().createIndex, @@ -126,14 +124,14 @@ describe('create-index module', function () { }, }, }); - await store.dispatch(createIndex()); + store.dispatch(createIndexFormSubmitted()); expect(store.getState().createIndex.error).to.equal( 'Bad ColumnstoreProjection: SyntaxError: Unexpected token \'o\', "not a colum"... is not valid JSON' ); }); - it('validates partial filter expression', async function () { + it('validates partial filter expression', function () { Object.assign(store.getState(), { createIndex: { ...store.getState().createIndex, @@ -147,124 +145,12 @@ describe('create-index module', function () { }, }, }); - await store.dispatch(createIndex()); + store.dispatch(createIndexFormSubmitted()); expect(store.getState().createIndex.error).to.equal( 'Bad PartialFilterExpression: SyntaxError: Unexpected end of JSON input' ); }); - - it('succeeds if dataService.createIndex() resolves', async function () { - let stateBeforeCreateIndex; - - const createIndexStub = sinon - .stub() - .callsFake(async (): Promise => { - // store it so we can assert on the in-between state - stateBeforeCreateIndex = { ...store.getState().createIndex }; - return Promise.resolve('ok'); - }); - - store = setupStore( - {}, - { - createIndex: createIndexStub, - } - ); - - store.dispatch(updateFieldName(0, 'foo')); - store.dispatch(fieldTypeUpdated(0, '1 (asc)')); - - store.dispatch(optionToggled('unique', true)); - store.dispatch(optionToggled('name', true)); - store.dispatch(optionChanged('name', 'my-index')); - store.dispatch(optionToggled('expireAfterSeconds', true)); - store.dispatch(optionChanged('expireAfterSeconds', '60')); - store.dispatch(optionToggled('partialFilterExpression', true)); - store.dispatch( - optionChanged('partialFilterExpression', '{ "rating": { "$gt": 5 } }') - ); - - await store.dispatch(createIndex()); - - // make sure it got to insert - expect(createIndexStub.callCount).to.equal(1); - - // it should have set it to be in progress before calling dataService.createIndex - expect(stateBeforeCreateIndex).to.deep.equal({ - inProgress: true, - isVisible: false, - error: null, - fields: [{ name: 'foo', type: '1 (asc)' }], - options: { - unique: { value: false, enabled: true }, - name: { value: 'my-index', enabled: true }, - expireAfterSeconds: { value: '60', enabled: true }, - partialFilterExpression: { - value: '{ "rating": { "$gt": 5 } }', - enabled: true, - }, - wildcardProjection: { value: '', enabled: false }, - collation: { value: '', enabled: false }, - columnstoreProjection: { value: '', enabled: false }, - sparse: { value: false, enabled: false }, - }, - }); - - const [ns, spec, options] = createIndexStub.args[0]; - expect(ns).to.equal('citibike.trips'); - expect(spec).to.deep.equal({ foo: 1 }); - expect(options).to.deep.equal({ - expireAfterSeconds: 60, - name: 'my-index', - partialFilterExpression: { - rating: { - $gt: 5, - }, - }, - }); - - expect(store.getState().createIndex).to.deep.equal(INITIAL_STATE); - }); - - it('fails if dataService.createIndex() rejects', async function () { - const createIndexStub = sinon - .stub() - .rejects(new Error('This is an error')); - - store = setupStore( - {}, - { - createIndex: createIndexStub, - } - ); - - store.dispatch(updateFieldName(0, 'foo')); - store.dispatch(fieldTypeUpdated(0, 'text')); - - await store.dispatch(createIndex()); - - // make sure it got to insert - expect(createIndexStub.callCount).to.equal(1); - - // state should be there with an error, not inProgress anymore - expect(store.getState().createIndex).to.deep.equal({ - inProgress: false, - isVisible: false, - error: 'This is an error', - fields: [{ name: 'foo', type: 'text' }], - options: { - unique: { value: false, enabled: false }, - name: { value: '', enabled: false }, - expireAfterSeconds: { value: '', enabled: false }, - partialFilterExpression: { value: '', enabled: false }, - wildcardProjection: { value: '', enabled: false }, - collation: { value: '', enabled: false }, - columnstoreProjection: { value: '', enabled: false }, - sparse: { value: false, enabled: false }, - }, - }); - }); }); describe('fieldAdded', function () { diff --git a/packages/compass-indexes/src/modules/create-index.tsx b/packages/compass-indexes/src/modules/create-index.tsx index f9ada33fa4a..fb5d380197c 100644 --- a/packages/compass-indexes/src/modules/create-index.tsx +++ b/packages/compass-indexes/src/modules/create-index.tsx @@ -1,15 +1,13 @@ import { EJSON, ObjectId } from 'bson'; -import type { CreateIndexesOptions, IndexSpecification } from 'mongodb'; +import type { CreateIndexesOptions } from 'mongodb'; import { isCollationValid } from 'mongodb-query-parser'; import React from 'react'; import type { Action, Reducer, Dispatch } from 'redux'; import { Badge } from '@mongodb-js/compass-components'; import { isAction } from '../utils/is-action'; -import type { InProgressIndex } from './regular-indexes'; import type { IndexesThunkAction } from '.'; -import { hasColumnstoreIndex } from '../utils/columnstore-indexes'; import type { RootState } from '.'; -import { refreshRegularIndexes } from './regular-indexes'; +import { createRegularIndex } from './regular-indexes'; export enum ActionTypes { FieldAdded = 'compass-indexes/create-index/fields/field-added', @@ -26,10 +24,7 @@ export enum ActionTypes { CreateIndexOpened = 'compass-indexes/create-index/create-index-shown', CreateIndexClosed = 'compass-indexes/create-index/create-index-hidden', - // These also get used by the regular-indexes slice's reducer - IndexCreationStarted = 'compass-indexes/create-index/index-creation-started', - IndexCreationSucceeded = 'compass-indexes/create-index/index-creation-succeeded', - IndexCreationFailed = 'compass-indexes/create-index/index-creation-failed', + CreateIndexFormSubmitted = 'compass-indexes/create-index/create-index-form-submitted', } // fields @@ -58,6 +53,10 @@ type FieldsChangedAction = { fields: Field[]; }; +/** + * Emitted only when the form fails client-side validation before being + * submitted + */ type ErrorEncounteredAction = { type: ActionTypes.ErrorEncountered; error: string; @@ -75,20 +74,12 @@ type CreateIndexClosedAction = { type: ActionTypes.CreateIndexClosed; }; -export type IndexCreationStartedAction = { - type: ActionTypes.IndexCreationStarted; - inProgressIndex: InProgressIndex; -}; - -export type IndexCreationSucceededAction = { - type: ActionTypes.IndexCreationSucceeded; - inProgressIndexId: string; -}; - -export type IndexCreationFailedAction = { - type: ActionTypes.IndexCreationFailed; - inProgressIndexId: string; - error: string; +/** + * Dispatched when the form passed the client validation and the form data was + * submitted for index creation + */ +type CreateIndexFormSubmittedAction = { + type: ActionTypes.CreateIndexFormSubmitted; }; export const fieldAdded = () => ({ @@ -259,11 +250,15 @@ const INITIAL_OPTIONS_STATE = Object.fromEntries( // other export type State = { - // modal state - inProgress: boolean; + // A unique id assigned to the create index modal on open, will be used when + // creating an instance of in-progress index and can be used to map the index + // to the form if needed + indexId: string; + + // Whether or not the modal is open or closed isVisible: boolean; - // validation + // Client-side validation error error: string | null; // form fields related @@ -274,15 +269,18 @@ export type State = { }; export const INITIAL_STATE: State = { - inProgress: false, + indexId: new ObjectId().toHexString(), isVisible: false, error: null, fields: INITIAL_FIELDS_STATE, options: INITIAL_OPTIONS_STATE, }; -function getInitialState() { - return JSON.parse(JSON.stringify(INITIAL_STATE)); +function getInitialState(): State { + return { + ...JSON.parse(JSON.stringify(INITIAL_STATE)), + indexId: new ObjectId().toHexString(), + }; } //------- @@ -304,67 +302,9 @@ export const errorCleared = (): ErrorClearedAction => ({ type: ActionTypes.ErrorCleared, }); -const indexCreationStarted = ( - inProgressIndex: InProgressIndex -): IndexCreationStartedAction => ({ - type: ActionTypes.IndexCreationStarted, - inProgressIndex, -}); - -const indexCreationSucceeded = ( - inProgressIndexId: string -): IndexCreationSucceededAction => ({ - type: ActionTypes.IndexCreationSucceeded, - inProgressIndexId, -}); - -const indexCreationFailed = ( - inProgressIndexId: string, - error: string -): IndexCreationFailedAction => ({ - type: ActionTypes.IndexCreationFailed, - inProgressIndexId, - error, -}); - export type CreateIndexSpec = { [key: string]: string | number; }; -const prepareIndex = ({ - ns, - name, - spec, -}: { - ns: string; - name?: string; - spec: CreateIndexSpec; -}): InProgressIndex => { - const inProgressIndexId = new ObjectId().toHexString(); - const inProgressIndexFields = Object.keys(spec).map((field: string) => ({ - field, - value: spec[field], - })); - const inProgressIndexName = - name || - Object.keys(spec).reduce((previousValue, currentValue) => { - return `${ - previousValue === '' ? '' : `${previousValue}_` - }${currentValue}_${spec[currentValue]}`; - }, ''); - return { - id: inProgressIndexId, - extra: { - status: 'inprogress', - }, - key: spec, - fields: inProgressIndexFields, - name: inProgressIndexName, - ns, - size: 0, - relativeSize: 0, - usageCount: 0, - }; -}; function isEmptyValue(value: unknown) { if (value === '') { @@ -377,24 +317,16 @@ function isEmptyValue(value: unknown) { return false; } -export const createIndex = (): IndexesThunkAction< - Promise, - | ErrorEncounteredAction - | IndexCreationStartedAction - | IndexCreationSucceededAction - | IndexCreationFailedAction +export const createIndexFormSubmitted = (): IndexesThunkAction< + void, + ErrorEncounteredAction | CreateIndexFormSubmittedAction > => { - return async ( - dispatch, - getState, - { dataService, track, connectionInfoRef } - ) => { - const state = getState(); + return (dispatch, getState) => { const spec = {} as CreateIndexSpec; // Check for field errors. if ( - state.createIndex.fields.some( + getState().createIndex.fields.some( (field: Field) => field.name === '' || field.type === '' ) ) { @@ -402,9 +334,9 @@ export const createIndex = (): IndexesThunkAction< return; } - const stateOptions = state.createIndex.options; + const formIndexOptions = getState().createIndex.options; - state.createIndex.fields.forEach((field: Field) => { + getState().createIndex.fields.forEach((field: Field) => { let type: string | number = field.type; if (field.type === '1 (asc)') type = 1; if (field.type === '-1 (desc)') type = -1; @@ -415,48 +347,48 @@ export const createIndex = (): IndexesThunkAction< // Check for collation errors. const collation = - isCollationValid(stateOptions.collation.value ?? '') || undefined; + isCollationValid(formIndexOptions.collation.value ?? '') || undefined; - if (stateOptions.collation.enabled && !collation) { + if (formIndexOptions.collation.enabled && !collation) { dispatch(errorEncountered('You must provide a valid collation object')); return; } - if (stateOptions.collation.enabled) { + if (formIndexOptions.collation.enabled) { options.collation = collation; } - if (stateOptions.unique.enabled) { - options.unique = stateOptions.unique.value; + if (formIndexOptions.unique.enabled) { + options.unique = formIndexOptions.unique.value; } - if (stateOptions.sparse.enabled) { - options.sparse = stateOptions.sparse.value; + if (formIndexOptions.sparse.enabled) { + options.sparse = formIndexOptions.sparse.value; } // The server will generate a name when we don't provide one. - if (stateOptions.name.enabled && stateOptions.name.value) { - options.name = stateOptions.name.value; + if (formIndexOptions.name.enabled && formIndexOptions.name.value) { + options.name = formIndexOptions.name.value; } - if (stateOptions.expireAfterSeconds.enabled) { + if (formIndexOptions.expireAfterSeconds.enabled) { options.expireAfterSeconds = Number( - stateOptions.expireAfterSeconds.value + formIndexOptions.expireAfterSeconds.value ); if (isNaN(options.expireAfterSeconds)) { dispatch( errorEncountered( - `Bad TTL: "${String(stateOptions.expireAfterSeconds.value)}"` + `Bad TTL: "${String(formIndexOptions.expireAfterSeconds.value)}"` ) ); return; } } - if (stateOptions.wildcardProjection.enabled) { + if (formIndexOptions.wildcardProjection.enabled) { try { options.wildcardProjection = EJSON.parse( - stateOptions.wildcardProjection.value ?? '' + formIndexOptions.wildcardProjection.value ?? '' ) as Document; } catch (err) { dispatch(errorEncountered(`Bad WildcardProjection: ${String(err)}`)); @@ -464,11 +396,11 @@ export const createIndex = (): IndexesThunkAction< } } - if (stateOptions.columnstoreProjection.enabled) { + if (formIndexOptions.columnstoreProjection.enabled) { try { // @ts-expect-error columnstoreProjection is not a part of CreateIndexesOptions yet. options.columnstoreProjection = EJSON.parse( - stateOptions.columnstoreProjection.value ?? '' + formIndexOptions.columnstoreProjection.value ?? '' ) as Document; } catch (err) { dispatch(errorEncountered(`Bad ColumnstoreProjection: ${String(err)}`)); @@ -476,10 +408,10 @@ export const createIndex = (): IndexesThunkAction< } } - if (stateOptions.partialFilterExpression.enabled) { + if (formIndexOptions.partialFilterExpression.enabled) { try { options.partialFilterExpression = EJSON.parse( - state.createIndex.options.partialFilterExpression.value ?? '' + formIndexOptions.partialFilterExpression.value ?? '' ) as Document; } catch (err) { dispatch( @@ -495,45 +427,18 @@ export const createIndex = (): IndexesThunkAction< // explicitly can lead to the server errors for some index types that don't // support them (even though technically user is not enabling them) for (const optionName of Object.keys( - stateOptions - ) as (keyof typeof stateOptions)[]) { - if (isEmptyValue(stateOptions[optionName].value)) { + formIndexOptions + ) as (keyof typeof formIndexOptions)[]) { + if (isEmptyValue(formIndexOptions[optionName].value)) { // @ts-expect-error columnstoreProjection is not a part of CreateIndexesOptions yet. delete options[optionName]; } } - const ns = state.namespace; - const inProgressIndex = prepareIndex({ ns, name: options.name, spec }); - - dispatch(indexCreationStarted(inProgressIndex)); - - const trackEvent = { - unique: options.unique, - ttl: stateOptions.expireAfterSeconds.enabled, - columnstore_index: hasColumnstoreIndex(state.createIndex.fields), - has_columnstore_projection: stateOptions.columnstoreProjection.enabled, - has_wildcard_projection: stateOptions.wildcardProjection.enabled, - custom_collation: stateOptions.collation.enabled, - geo: - state.createIndex.fields.filter( - ({ type }: { type: string }) => type === '2dsphere' - ).length > 0, - atlas_search: false, - }; - - try { - await dataService.createIndex(ns, spec as IndexSpecification, options); - dispatch(indexCreationSucceeded(inProgressIndex.id)); - track('Index Created', trackEvent, connectionInfoRef.current); - - // Start a new fetch so that the newly added index's details can be - // loaded. indexCreationSucceeded() will remove the in-progress one, but - // we still need the new info. - await dispatch(refreshRegularIndexes()); - } catch (err) { - dispatch(indexCreationFailed(inProgressIndex.id, (err as Error).message)); - } + dispatch({ type: ActionTypes.CreateIndexFormSubmitted }); + void dispatch( + createRegularIndex(getState().createIndex.indexId, spec, options) + ); }; }; @@ -609,7 +514,7 @@ const reducer: Reducer = (state = INITIAL_STATE, action) => { isAction(action, ActionTypes.CreateIndexOpened) ) { return { - ...state, + ...getInitialState(), isVisible: true, }; } @@ -638,34 +543,17 @@ const reducer: Reducer = (state = INITIAL_STATE, action) => { } if ( - isAction( + isAction( action, - ActionTypes.IndexCreationStarted + ActionTypes.CreateIndexFormSubmitted ) ) { return { ...state, - error: null, - inProgress: true, - }; - } - if ( - isAction( - action, - ActionTypes.IndexCreationSucceeded - ) - ) { - return { - ...getInitialState(), + isVisible: false, }; } - if ( - isAction(action, ActionTypes.IndexCreationFailed) - ) { - return { ...state, inProgress: false, error: action.error }; - } - return state; }; diff --git a/packages/compass-indexes/src/modules/regular-indexes.ts b/packages/compass-indexes/src/modules/regular-indexes.ts index e51e1bcdcaa..042dc2775a1 100644 --- a/packages/compass-indexes/src/modules/regular-indexes.ts +++ b/packages/compass-indexes/src/modules/regular-indexes.ts @@ -11,18 +11,14 @@ import type { FetchStatus } from '../utils/fetch-status'; import { FetchReasons } from '../utils/fetch-reason'; import type { FetchReason } from '../utils/fetch-reason'; import { isAction } from '../utils/is-action'; -import { ActionTypes as CreateIndexActionTypes } from './create-index'; -import type { - CreateIndexSpec, - IndexCreationStartedAction, - IndexCreationSucceededAction, - IndexCreationFailedAction, -} from './create-index'; +import type { CreateIndexSpec } from './create-index'; import type { IndexesThunkAction, RootState } from '.'; import { hideModalDescription, unhideModalDescription, } from '../utils/modal-descriptions'; +import type { IndexSpecification, CreateIndexesOptions } from 'mongodb'; +import { hasColumnstoreIndex } from '../utils/columnstore-indexes'; export type RegularIndex = Omit< IndexDefinition, @@ -45,6 +41,44 @@ export type InProgressIndex = { }; }; +const prepareInProgressIndex = ( + id: string, + { + ns, + name, + spec, + }: { + ns: string; + name?: string; + spec: CreateIndexSpec; + } +): InProgressIndex => { + const inProgressIndexFields = Object.keys(spec).map((field: string) => ({ + field, + value: spec[field], + })); + const inProgressIndexName = + name || + Object.keys(spec).reduce((previousValue, currentValue) => { + return `${ + previousValue === '' ? '' : `${previousValue}_` + }${currentValue}_${spec[currentValue]}`; + }, ''); + return { + id, + extra: { + status: 'inprogress', + }, + key: spec, + fields: inProgressIndexFields, + name: inProgressIndexName, + ns, + size: 0, + relativeSize: 0, + usageCount: 0, + }; +}; + export enum ActionTypes { IndexesOpened = 'compass-indexes/regular-indexes/indexes-opened', IndexesClosed = 'compass-indexes/regular-indexes/indexes-closed', @@ -53,10 +87,14 @@ export enum ActionTypes { FetchIndexesSucceeded = 'compass-indexes/regular-indexes/fetch-indexes-succeeded', FetchIndexesFailed = 'compass-indexes/regular-indexes/fetch-indexes-failed', - // Basically the same thing as CreateIndexActionTypes.IndexCreationSucceeded + // Basically the same thing as ActionTypes.IndexCreationSucceeded // in that it will remove the index, but it is for manually removing the row // of an index that failed FailedIndexRemoved = 'compass-indexes/regular-indexes/failed-index-removed', + + IndexCreationStarted = 'compass-indexes/create-index/index-creation-started', + IndexCreationSucceeded = 'compass-indexes/create-index/index-creation-succeeded', + IndexCreationFailed = 'compass-indexes/create-index/index-creation-failed', } type IndexesOpenedAction = { @@ -82,6 +120,22 @@ type FetchIndexesFailedAction = { error: string; }; +type IndexCreationStartedAction = { + type: ActionTypes.IndexCreationStarted; + inProgressIndex: InProgressIndex; +}; + +type IndexCreationSucceededAction = { + type: ActionTypes.IndexCreationSucceeded; + inProgressIndexId: string; +}; + +type IndexCreationFailedAction = { + type: ActionTypes.IndexCreationFailed; + inProgressIndexId: string; + error: string; +}; + type FailedIndexRemovedAction = { type: ActionTypes.FailedIndexRemoved; inProgressIndexId: string; @@ -171,7 +225,7 @@ export default function reducer( if ( isAction( action, - CreateIndexActionTypes.IndexCreationStarted + ActionTypes.IndexCreationStarted ) ) { // Add the new in-progress index to the in-progress indexes. @@ -196,7 +250,7 @@ export default function reducer( if ( isAction( action, - CreateIndexActionTypes.IndexCreationSucceeded + ActionTypes.IndexCreationSucceeded ) || isAction(action, ActionTypes.FailedIndexRemoved) ) { @@ -212,10 +266,7 @@ export default function reducer( } if ( - isAction( - action, - CreateIndexActionTypes.IndexCreationFailed - ) + isAction(action, ActionTypes.IndexCreationFailed) ) { const idx = state.inProgressIndexes.findIndex( (x) => x.id === action.inProgressIndexId @@ -323,6 +374,7 @@ const fetchIndexes = ( } }; }; + export const fetchRegularIndexes = (): IndexesThunkAction< Promise, FetchIndexesActions @@ -382,6 +434,89 @@ export const stopPollingRegularIndexes = (tabId: string) => { }; }; +const indexCreationStarted = ( + inProgressIndex: InProgressIndex +): IndexCreationStartedAction => ({ + type: ActionTypes.IndexCreationStarted, + inProgressIndex, +}); + +const indexCreationSucceeded = ( + inProgressIndexId: string +): IndexCreationSucceededAction => ({ + type: ActionTypes.IndexCreationSucceeded, + inProgressIndexId, +}); + +const indexCreationFailed = ( + inProgressIndexId: string, + error: string +): IndexCreationFailedAction => ({ + type: ActionTypes.IndexCreationFailed, + inProgressIndexId, + error, +}); + +export function createRegularIndex( + inProgressIndexId: string, + spec: CreateIndexSpec, + options: CreateIndexesOptions +): IndexesThunkAction< + Promise, + | IndexCreationStartedAction + | IndexCreationSucceededAction + | IndexCreationFailedAction +> { + return async ( + dispatch, + getState, + { track, dataService, connectionInfoRef } + ) => { + const ns = getState().namespace; + const inProgressIndex = prepareInProgressIndex(inProgressIndexId, { + ns, + name: options.name, + spec, + }); + + dispatch(indexCreationStarted(inProgressIndex)); + + const fieldsFromSpec = Object.entries(spec).map(([k, v]) => { + return { name: k, type: String(v) }; + }); + + const trackEvent = { + unique: options.unique, + ttl: typeof options.expireAfterSeconds !== 'undefined', + columnstore_index: hasColumnstoreIndex(fieldsFromSpec), + has_columnstore_projection: + // @ts-expect-error columnstoreProjection is not a part of + // CreateIndexesOptions yet. + typeof options.columnstoreProjection !== 'undefined', + has_wildcard_projection: + typeof options.wildcardProjection !== 'undefined', + custom_collation: typeof options.collation !== 'undefined', + geo: fieldsFromSpec.some(({ type }) => { + return type === '2dsphere'; + }), + atlas_search: false, + }; + + try { + await dataService.createIndex(ns, spec as IndexSpecification, options); + dispatch(indexCreationSucceeded(inProgressIndexId)); + track('Index Created', trackEvent, connectionInfoRef.current); + + // Start a new fetch so that the newly added index's details can be + // loaded. indexCreationSucceeded() will remove the in-progress one, but + // we still need the new info. + await dispatch(refreshRegularIndexes()); + } catch (err) { + dispatch(indexCreationFailed(inProgressIndexId, (err as Error).message)); + } + }; +} + const failedIndexRemoved = ( inProgressIndexId: string ): FailedIndexRemovedAction => ({