diff --git a/packages/compass-components/src/hooks/use-confirmation.tsx b/packages/compass-components/src/hooks/use-confirmation.tsx index e045a1be39d..f4665ee23db 100644 --- a/packages/compass-components/src/hooks/use-confirmation.tsx +++ b/packages/compass-components/src/hooks/use-confirmation.tsx @@ -12,6 +12,7 @@ type ConfirmationProperties = { description: React.ReactNode; buttonText?: string; variant?: ConfirmationModalVariant; + requiredInputText?: string; }; type ConfirmationCallback = (value: boolean) => void; @@ -134,6 +135,7 @@ export const ConfirmationModalArea: React.FC = ({ children }) => { title={confirmationProps.title ?? 'Are you sure?'} variant={confirmationProps.variant ?? ConfirmationModalVariant.Default} buttonText={confirmationProps.buttonText ?? 'Confirm'} + requiredInputText={confirmationProps.requiredInputText ?? undefined} onConfirm={handleConfirm} onCancel={handleCancel} > diff --git a/packages/compass-indexes/src/components/indexes/indexes.spec.tsx b/packages/compass-indexes/src/components/indexes/indexes.spec.tsx index fe2b86a47ca..3162a86cece 100644 --- a/packages/compass-indexes/src/components/indexes/indexes.spec.tsx +++ b/packages/compass-indexes/src/components/indexes/indexes.spec.tsx @@ -12,11 +12,11 @@ import { expect } from 'chai'; import sinon from 'sinon'; import preferencesAccess from 'compass-preferences-model'; import type { RegularIndex } from '../../modules/regular-indexes'; -import type { SearchIndex } from 'mongodb-data-service'; import type Store from '../../stores'; import type { IndexesDataService } from '../../stores/store'; import Indexes from './indexes'; import { setupStore } from '../../../test/setup-store'; +import { searchIndexes } from '../../../test/fixtures/search-indexes'; const renderIndexes = (props: Partial = {}) => { const store = setupStore(); @@ -279,25 +279,8 @@ describe('Indexes Component', function () { it('renders the search indexes table if the current view changes to search indexes', async function () { const store = renderIndexes(); - const indexes: SearchIndex[] = [ - { - id: '1', - name: 'default', - status: 'READY', - queryable: true, - latestDefinition: {}, - }, - { - id: '2', - name: 'another', - status: 'READY', - queryable: true, - latestDefinition: {}, - }, - ]; - store.getState()!.dataService!.getSearchIndexes = function () { - return Promise.resolve(indexes); + return Promise.resolve(searchIndexes); }; // switch to the Search Indexes tab diff --git a/packages/compass-indexes/src/components/indexes/indexes.tsx b/packages/compass-indexes/src/components/indexes/indexes.tsx index 54bf38e07b2..c655a49e678 100644 --- a/packages/compass-indexes/src/components/indexes/indexes.tsx +++ b/packages/compass-indexes/src/components/indexes/indexes.tsx @@ -16,7 +16,7 @@ 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 '../create-search-index-modal'; +import { CreateSearchIndexModal } 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. diff --git a/packages/compass-indexes/src/components/create-search-index-modal/index.spec.tsx b/packages/compass-indexes/src/components/search-indexes-modals/create-search-index-modal.spec.tsx similarity index 95% rename from packages/compass-indexes/src/components/create-search-index-modal/index.spec.tsx rename to packages/compass-indexes/src/components/search-indexes-modals/create-search-index-modal.spec.tsx index c3f90af875b..1b67222fd8d 100644 --- a/packages/compass-indexes/src/components/create-search-index-modal/index.spec.tsx +++ b/packages/compass-indexes/src/components/search-indexes-modals/create-search-index-modal.spec.tsx @@ -1,6 +1,6 @@ import { expect } from 'chai'; -import { DEFAULT_INDEX_DEFINITION } from '.'; -import CreateSearchIndexModal from '.'; +import { DEFAULT_INDEX_DEFINITION } from './create-search-index-modal'; +import CreateSearchIndexModal from './create-search-index-modal'; import sinon from 'sinon'; import { Provider } from 'react-redux'; diff --git a/packages/compass-indexes/src/components/create-search-index-modal/index.tsx b/packages/compass-indexes/src/components/search-indexes-modals/create-search-index-modal.tsx similarity index 100% rename from packages/compass-indexes/src/components/create-search-index-modal/index.tsx rename to packages/compass-indexes/src/components/search-indexes-modals/create-search-index-modal.tsx diff --git a/packages/compass-indexes/src/components/search-indexes-modals/index.ts b/packages/compass-indexes/src/components/search-indexes-modals/index.ts new file mode 100644 index 00000000000..81595e84444 --- /dev/null +++ b/packages/compass-indexes/src/components/search-indexes-modals/index.ts @@ -0,0 +1 @@ +export { default as CreateSearchIndexModal } from './create-search-index-modal'; 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 new file mode 100644 index 00000000000..ebefc3e34b6 --- /dev/null +++ b/packages/compass-indexes/src/components/search-indexes-table/search-index-actions.spec.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { cleanup, render, screen } from '@testing-library/react'; +import { expect } from 'chai'; +import { spy } from 'sinon'; +import type { SinonSpy } from 'sinon'; +import userEvent from '@testing-library/user-event'; +import type { SearchIndex } from 'mongodb-data-service'; + +import SearchIndexActions from './search-index-actions'; + +describe('SearchIndexActions Component', function () { + let onDropSpy: SinonSpy; + + before(cleanup); + afterEach(cleanup); + beforeEach(function () { + onDropSpy = 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( + 'Drop Index artist_id_index' + ); + expect(onDropSpy.callCount).to.equal(0); + userEvent.click(button); + expect(onDropSpy.callCount).to.equal(1); + }); +}); 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 new file mode 100644 index 00000000000..42afd7e3f87 --- /dev/null +++ b/packages/compass-indexes/src/components/search-indexes-table/search-index-actions.tsx @@ -0,0 +1,47 @@ +import React, { useCallback, useMemo } from 'react'; +import type { GroupedItemAction } from '@mongodb-js/compass-components'; +import { ItemActionGroup } from '@mongodb-js/compass-components'; +import type { SearchIndex } from 'mongodb-data-service'; + +type IndexActionsProps = { + index: SearchIndex; + onDropIndex: (name: string) => void; +}; + +type SearchIndexAction = 'drop'; + +const IndexActions: React.FunctionComponent = ({ + index, + onDropIndex, +}) => { + const indexActions: GroupedItemAction[] = useMemo(() => { + const actions: GroupedItemAction[] = [ + { + action: 'drop', + label: `Drop Index ${index.name}`, + icon: 'Trash', + }, + ]; + + return actions; + }, [index]); + + const onAction = useCallback( + (action: SearchIndexAction) => { + if (action === 'drop') { + void onDropIndex(index.name); + } + }, + [onDropIndex, index] + ); + + return ( + + data-testid="search-index-actions" + actions={indexActions} + onAction={onAction} + > + ); +}; + +export default IndexActions; 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 fe4b8783a1f..932ed587dac 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 @@ -1,9 +1,9 @@ import React from 'react'; import { cleanup, - fireEvent, render, screen, + fireEvent, within, } from '@testing-library/react'; import { expect } from 'chai'; @@ -11,25 +11,8 @@ import sinon from 'sinon'; import userEvent from '@testing-library/user-event'; import { SearchIndexesTable } from './search-indexes-table'; -import type { SearchIndex } from 'mongodb-data-service'; import { SearchIndexesStatuses } from '../../modules/search-indexes'; - -const indexes: SearchIndex[] = [ - { - id: '1', - name: 'default', - status: 'READY', - queryable: true, - latestDefinition: {}, - }, - { - id: '2', - name: 'another', - status: 'READY', - queryable: true, - latestDefinition: {}, - }, -]; +import { searchIndexes as indexes } from './../../../test/fixtures/search-indexes'; const renderIndexList = ( props: Partial> = {} @@ -44,6 +27,7 @@ const renderIndexList = ( isWritable={true} readOnly={false} onSortTable={onSortTableSpy} + onDropIndex={() => {}} openCreateModal={openCreateSpy} {...props} /> @@ -89,10 +73,7 @@ describe('SearchIndexesTable Component', function () { }); } - for (const status of [ - SearchIndexesStatuses.PENDING, - SearchIndexesStatuses.ERROR, - ]) { + for (const status of [SearchIndexesStatuses.PENDING]) { it(`does not render the list if the status is ${status}`, function () { renderIndexList({ status, @@ -145,4 +126,19 @@ describe('SearchIndexesTable Component', function () { expect(onSortTableSpy.getCalls()[1].args).to.deep.equal([column, 'asc']); }); } + + context('renders list with action', function () { + it('renders drop action and shows modal when clicked', function () { + const onDropIndexSpy = sinon.spy(); + + renderIndexList({ onDropIndex: onDropIndexSpy }); + const dropIndexActions = screen.getAllByTestId( + 'search-index-actions-drop-action' + ); + + expect(dropIndexActions.length).to.equal(indexes.length); + dropIndexActions[0].click(); + expect(onDropIndexSpy.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 f958235bacf..3d53af013cb 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 @@ -14,6 +14,7 @@ import { import type { SearchSortColumn } from '../../modules/search-indexes'; import { SearchIndexesStatuses, + dropSearchIndex, openModalForCreation, } from '../../modules/search-indexes'; import type { SearchIndexesStatus } from '../../modules/search-indexes'; @@ -21,6 +22,7 @@ import { sortSearchIndexes } from '../../modules/search-indexes'; import type { SortDirection, RootState } from '../../modules'; import { IndexesTable } from '../indexes-table'; +import IndexActions from './search-index-actions'; import { ZeroGraphic } from './zero-graphic'; type SearchIndexesTableProps = { @@ -28,6 +30,7 @@ type SearchIndexesTableProps = { isWritable?: boolean; readOnly?: boolean; onSortTable: (column: SearchSortColumn, direction: SortDirection) => void; + onDropIndex: (name: string) => void; openCreateModal: () => void; status: SearchIndexesStatus; }; @@ -102,6 +105,7 @@ export const SearchIndexesTable: React.FunctionComponent< onSortTable, openCreateModal, status, + onDropIndex, }) => { if (!isReadyStatus(status)) { // If there's an error or the search indexes are still pending or search @@ -137,7 +141,7 @@ export const SearchIndexesTable: React.FunctionComponent< ), }, ], - + actions: , // TODO(COMPASS-7206): details for the nested row }; }); @@ -162,6 +166,7 @@ const mapState = ({ searchIndexes, isWritable }: RootState) => ({ const mapDispatch = { onSortTable: sortSearchIndexes, + onDropIndex: dropSearchIndex, openCreateModal: openModalForCreation, }; diff --git a/packages/compass-indexes/src/modules/search-indexes.spec.ts b/packages/compass-indexes/src/modules/search-indexes.spec.ts index 8933882a4ca..57ede53cb64 100644 --- a/packages/compass-indexes/src/modules/search-indexes.spec.ts +++ b/packages/compass-indexes/src/modules/search-indexes.spec.ts @@ -6,29 +6,16 @@ import { saveIndex, fetchSearchIndexes, sortSearchIndexes, + dropSearchIndex, } from './search-indexes'; import { setupStore } from '../../test/setup-store'; +import { searchIndexes } from '../../test/fixtures/search-indexes'; import sinon from 'sinon'; import type { IndexesDataService } from '../stores/store'; -import type { SearchIndex } from 'mongodb-data-service'; import { readonlyViewChanged } from './is-readonly-view'; -const searchIndexes: SearchIndex[] = [ - { - id: '1', - name: 'default', - status: 'READY', - queryable: true, - latestDefinition: {}, - }, - { - id: '2', - name: 'another', - status: 'FAILED', - queryable: true, - latestDefinition: {}, - }, -]; +// Importing this to stub showConfirmation +import * as searchIndexesSlice from './search-indexes'; describe('search-indexes module', function () { let store: ReturnType; @@ -127,7 +114,7 @@ describe('search-indexes module', function () { id: '2', name: 'another', status: 'FAILED', - queryable: true, + queryable: false, latestDefinition: {}, }, { @@ -186,7 +173,7 @@ describe('search-indexes module', function () { id: '2', name: 'another', status: 'FAILED', - queryable: true, + queryable: false, latestDefinition: {}, }, ]); @@ -209,4 +196,38 @@ describe('search-indexes module', function () { expect(store.getState().searchIndexes.createIndex.isModalOpen).to.be.false; expect(dataProvider.createSearchIndex).to.have.been.calledOnce; }); + + context('drop search index', function () { + let dropSearchIndexStub: sinon.SinonStub; + let showConfirmationStub: sinon.SinonStub; + beforeEach(function () { + dropSearchIndexStub = sinon.stub( + store.getState().dataService as IndexesDataService, + 'dropSearchIndex' + ); + showConfirmationStub = sinon.stub(searchIndexesSlice, 'showConfirmation'); + }); + + afterEach(function () { + showConfirmationStub.restore(); + dropSearchIndexStub.restore(); + }); + + it('does not drop index when user does not confirm', async function () { + showConfirmationStub.resolves(false); + await store.dispatch(dropSearchIndex('index_name')); + expect(dropSearchIndexStub.callCount).to.equal(0); + }); + + it('drops index successfully', async function () { + showConfirmationStub.resolves(true); + dropSearchIndexStub.resolves(true); + await store.dispatch(dropSearchIndex('index_name')); + expect(dropSearchIndexStub.firstCall.args).to.deep.equal([ + 'citibike.trips', + 'index_name', + ]); + expect(store.getState().searchIndexes.error).to.be.undefined; + }); + }); }); diff --git a/packages/compass-indexes/src/modules/search-indexes.ts b/packages/compass-indexes/src/modules/search-indexes.ts index 1adb529f3b9..ed3ef7a1d65 100644 --- a/packages/compass-indexes/src/modules/search-indexes.ts +++ b/packages/compass-indexes/src/modules/search-indexes.ts @@ -1,7 +1,10 @@ import type { AnyAction } from 'redux'; import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; import { isAction } from './../utils/is-action'; -import { openToast } from '@mongodb-js/compass-components'; +import { + openToast, + showConfirmation as showConfirmationModal, +} from '@mongodb-js/compass-components'; import type { Document, MongoServerError } from 'mongodb'; const ATLAS_SEARCH_SERVER_ERRORS: Record = { @@ -168,7 +171,6 @@ export default function reducer( if (isAction(action, ActionTypes.SetError)) { return { ...state, - indexes: [], error: action.error, status: SearchIndexesStatuses.ERROR, }; @@ -246,7 +248,6 @@ export default function reducer( }, }; } - return state; } @@ -308,12 +309,13 @@ export const saveIndex = ( } dispatch(createIndexSucceeded()); - openToast('index-creation-in-progress', { + openToast('search-index-creation-in-progress', { title: `Your index ${indexName} is in progress.`, dismissible: true, timeout: 5000, variant: 'success', }); + void dispatch(fetchSearchIndexes()); }; }; const setError = (error: string | undefined): SetErrorAction => ({ @@ -387,6 +389,51 @@ export const sortSearchIndexes = ( }; }; +// Exporting this for test only to stub it and set +// its value. This enables to test dropSearchIndex action. +export const showConfirmation = showConfirmationModal; +export const dropSearchIndex = ( + name: string +): IndexesThunkAction> => { + return async function (dispatch, getState) { + const { namespace, dataService } = getState(); + if (!dataService) { + return; + } + + const isConfirmed = await showConfirmation({ + title: `Are you sure you want to drop "${name}" from Cluster?`, + buttonText: 'Drop Index', + variant: 'danger', + requiredInputText: name, + description: + 'If you drop default, all queries using it will no longer function', + }); + if (!isConfirmed) { + return; + } + + try { + await dataService.dropSearchIndex(namespace, name); + openToast('search-index-delete-in-progress', { + title: `Your index ${name} is being deleted.`, + dismissible: true, + timeout: 5000, + variant: 'success', + }); + void dispatch(fetchSearchIndexes()); + } catch (e) { + openToast('search-index-delete-failed', { + title: `Failed to drop index.`, + description: (e as Error).message, + dismissible: true, + timeout: 5000, + variant: 'warning', + }); + } + }; +}; + function _sortIndexes( indexes: SearchIndex[], column: SearchSortColumn, diff --git a/packages/compass-indexes/test/fixtures/search-indexes.ts b/packages/compass-indexes/test/fixtures/search-indexes.ts new file mode 100644 index 00000000000..39e6dae8c93 --- /dev/null +++ b/packages/compass-indexes/test/fixtures/search-indexes.ts @@ -0,0 +1,17 @@ +import type { SearchIndex } from 'mongodb-data-service'; +export const searchIndexes: SearchIndex[] = [ + { + id: '1', + name: 'default', + status: 'READY', + queryable: true, + latestDefinition: {}, + }, + { + id: '2', + name: 'another', + status: 'FAILED', + queryable: false, + latestDefinition: {}, + }, +];