diff --git a/packages/compass-components/src/hooks/use-confirmation.tsx b/packages/compass-components/src/hooks/use-confirmation.tsx index f4665ee23db..c1e4535dbab 100644 --- a/packages/compass-components/src/hooks/use-confirmation.tsx +++ b/packages/compass-components/src/hooks/use-confirmation.tsx @@ -1,5 +1,4 @@ import React, { useContext, useEffect, useRef, useState } from 'react'; - import { default as ConfirmationModal, Variant as ConfirmationModalVariant, @@ -7,13 +6,18 @@ import { export { ConfirmationModalVariant }; -type ConfirmationProperties = { - title: string; - description: React.ReactNode; - buttonText?: string; - variant?: ConfirmationModalVariant; - requiredInputText?: string; +type ConfirmationModalProps = React.ComponentProps; + +type ConfirmationProperties = Partial< + Pick< + ConfirmationModalProps, + 'title' | 'buttonText' | 'variant' | 'requiredInputText' + > +> & { + description?: React.ReactNode; + 'data-testid'?: string; }; + type ConfirmationCallback = (value: boolean) => void; interface ConfirmationModalContextData { @@ -130,7 +134,7 @@ export const ConfirmationModalArea: React.FC = ({ children }) => { {children} { ); }; +const ToastAreaMountedContext = React.createContext(false); + export const ToastArea: React.FunctionComponent = ({ children }) => { + if (useContext(ToastAreaMountedContext)) { + return <>{children}; + } + return ( - - <_ToastArea>{children} - + + + <_ToastArea>{children} + + ); }; diff --git a/packages/compass-e2e-tests/helpers/commands/drop-collection-from-sidebar.ts b/packages/compass-e2e-tests/helpers/commands/drop-collection-from-sidebar.ts index e8e6573d701..66bf5d3f766 100644 --- a/packages/compass-e2e-tests/helpers/commands/drop-collection-from-sidebar.ts +++ b/packages/compass-e2e-tests/helpers/commands/drop-collection-from-sidebar.ts @@ -38,7 +38,7 @@ export async function dropCollectionFromSidebar( await browser.clickVisible(Selectors.CollectionShowActionsButton); await browser.clickVisible(Selectors.DropCollectionButton); - await browser.dropCollection(collectionName); + await browser.dropNamespace(collectionName); // wait for it to be gone await collectionElement.waitForExist({ reverse: true }); diff --git a/packages/compass-e2e-tests/helpers/commands/drop-collection.ts b/packages/compass-e2e-tests/helpers/commands/drop-collection.ts deleted file mode 100644 index bee907092c7..00000000000 --- a/packages/compass-e2e-tests/helpers/commands/drop-collection.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { CompassBrowser } from '../compass-browser'; -import * as Selectors from '../selectors'; - -export async function dropCollection( - browser: CompassBrowser, - collectionName: string -): Promise { - const dropModalElement = await browser.$(Selectors.DropCollectionModal); - await dropModalElement.waitForDisplayed(); - const confirmInput = await browser.$(Selectors.DropCollectionConfirmName); - await confirmInput.setValue(collectionName); - const confirmButton = await browser.$(Selectors.DropCollectionDropButton); - await confirmButton.waitForEnabled(); - - await browser.screenshot('drop-collection-modal.png'); - - await confirmButton.click(); - await dropModalElement.waitForDisplayed({ reverse: true }); -} diff --git a/packages/compass-e2e-tests/helpers/commands/drop-database-from-sidebar.ts b/packages/compass-e2e-tests/helpers/commands/drop-database-from-sidebar.ts index cc2b6fbc5ae..422b7a53d01 100644 --- a/packages/compass-e2e-tests/helpers/commands/drop-database-from-sidebar.ts +++ b/packages/compass-e2e-tests/helpers/commands/drop-database-from-sidebar.ts @@ -18,7 +18,7 @@ export async function dropDatabaseFromSidebar( await browser.clickVisible(Selectors.DropDatabaseButton); - await browser.dropDatabase(dbName); + await browser.dropNamespace(dbName); // wait for it to be gone await browser diff --git a/packages/compass-e2e-tests/helpers/commands/drop-database.ts b/packages/compass-e2e-tests/helpers/commands/drop-database.ts deleted file mode 100644 index 359d43b215a..00000000000 --- a/packages/compass-e2e-tests/helpers/commands/drop-database.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { CompassBrowser } from '../compass-browser'; -import * as Selectors from '../selectors'; - -export async function dropDatabase( - browser: CompassBrowser, - dbName: string -): Promise { - const dropModalElement = await browser.$(Selectors.DropDatabaseModal); - await dropModalElement.waitForDisplayed(); - const confirmInput = await browser.$(Selectors.DropDatabaseConfirmName); - await confirmInput.setValue(dbName); - const confirmButton = await browser.$(Selectors.DropDatabaseDropButton); - await confirmButton.waitForEnabled(); - - await browser.screenshot('drop-database-modal.png'); - - await confirmButton.click(); - await dropModalElement.waitForDisplayed({ reverse: true }); -} diff --git a/packages/compass-e2e-tests/helpers/commands/drop-namespace.ts b/packages/compass-e2e-tests/helpers/commands/drop-namespace.ts new file mode 100644 index 00000000000..a964fd4ba4b --- /dev/null +++ b/packages/compass-e2e-tests/helpers/commands/drop-namespace.ts @@ -0,0 +1,23 @@ +import type { CompassBrowser } from '../compass-browser'; +import * as Selectors from '../selectors'; + +export async function dropNamespace( + browser: CompassBrowser, + collectionName: string +): Promise { + const dropModalElement = await browser.$(Selectors.DropNamespaceModal); + await dropModalElement.waitForDisplayed(); + const confirmInput = await browser.$(Selectors.DropNamespaceConfirmNameInput); + await confirmInput.setValue(collectionName); + const confirmButton = await browser.$(Selectors.DropNamespaceDropButton); + await confirmButton.waitForEnabled(); + + await browser.screenshot('drop-namespace-modal.png'); + + await confirmButton.click(); + + const successToast = browser.$(Selectors.DropNamespaceSuccessToast); + await successToast.waitForDisplayed(); + await browser.clickVisible(Selectors.DropNamespaceSuccessToastCloseButton); + await successToast.waitForDisplayed({ reverse: true }); +} diff --git a/packages/compass-e2e-tests/helpers/commands/index.ts b/packages/compass-e2e-tests/helpers/commands/index.ts index 192cdb2bd0a..a481ec21f46 100644 --- a/packages/compass-e2e-tests/helpers/commands/index.ts +++ b/packages/compass-e2e-tests/helpers/commands/index.ts @@ -25,8 +25,7 @@ export * from './do-connect'; export * from './hover'; export * from './add-database'; export * from './add-collection'; -export * from './drop-database'; -export * from './drop-collection'; +export * from './drop-namespace'; export * from './get-query-id'; export * from './run-find'; export * from './export-to-language'; diff --git a/packages/compass-e2e-tests/helpers/selectors.ts b/packages/compass-e2e-tests/helpers/selectors.ts index f17a356084f..461275c20ee 100644 --- a/packages/compass-e2e-tests/helpers/selectors.ts +++ b/packages/compass-e2e-tests/helpers/selectors.ts @@ -290,13 +290,6 @@ export const CreateDatabaseCreateButton = export const CreateDatabaseCancelButton = '[data-testid="create-database-modal"] [data-testid="cancel-button"]'; -// Drop database modal -export const DropDatabaseModal = '[data-testid="drop-database-modal"]'; -export const DropDatabaseConfirmName = - '[data-testid="confirm-drop-database-name"]'; -export const DropDatabaseDropButton = - '[data-testid="drop-database-modal"] [data-testid="submit-button"]'; - // Create collection modal export const CreateCollectionModal = '[data-testid="create-collection-modal"]'; export const CreateCollectionCollectionName = '[data-testid="collection-name"]'; @@ -361,12 +354,16 @@ export const createCollectionCustomCollationFieldMenu = ( return `[data-testid="use-custom-collation-fields"] #collation-field-${fieldName}-menu`; }; -// Drop collection modal -export const DropCollectionModal = '[data-testid="drop-collection-modal"]'; -export const DropCollectionConfirmName = - '[data-testid="confirm-drop-collection-name"]'; -export const DropCollectionDropButton = - '[data-testid="drop-collection-modal"] [data-testid="submit-button"]'; +// Drop namespace modal +export const DropNamespaceModal = + '[data-testid="drop-namespace-confirmation-modal"]'; +export const DropNamespaceConfirmNameInput = `${DropNamespaceModal} input`; +export const DropNamespaceDropButton = `${DropNamespaceModal} button:first-of-type`; +export const DropNamespaceCancelButton = `${DropNamespaceModal} button:last-of-type`; +export const DropNamespaceSuccessToast = + '[data-testid="toast-drop-namespace-success"]'; +export const DropNamespaceSuccessToastCloseButton = + '[data-testid="toast-drop-namespace-success"] [data-testid="lg-toast-dismiss-button"]'; // Shell export const ShellSection = '[data-testid="shell-section"]'; diff --git a/packages/compass-e2e-tests/tests/database-collections-tab.test.ts b/packages/compass-e2e-tests/tests/database-collections-tab.test.ts index 3adad4c8441..5221a5454b7 100644 --- a/packages/compass-e2e-tests/tests/database-collections-tab.test.ts +++ b/packages/compass-e2e-tests/tests/database-collections-tab.test.ts @@ -128,7 +128,7 @@ describe('Database collections tab', function () { await browser.clickVisible(Selectors.CollectionCardDrop); - await browser.dropCollection(collectionName); + await browser.dropNamespace(collectionName); // wait for it to be gone await collectionCard.waitForExist({ reverse: true }); diff --git a/packages/compass-e2e-tests/tests/instance-databases-tab.test.ts b/packages/compass-e2e-tests/tests/instance-databases-tab.test.ts index 3558a89c9af..ef42dca602a 100644 --- a/packages/compass-e2e-tests/tests/instance-databases-tab.test.ts +++ b/packages/compass-e2e-tests/tests/instance-databases-tab.test.ts @@ -123,7 +123,7 @@ describe('Instance databases tab', function () { await browser.clickVisible(Selectors.DatabaseCardDrop); - await browser.dropDatabase(dbName); + await browser.dropNamespace(dbName); // wait for it to be gone (which it will be anyway because the app should // redirect back to the databases tab) diff --git a/packages/compass-home/src/components/home.tsx b/packages/compass-home/src/components/home.tsx index 8af4b510e99..6652d73d087 100644 --- a/packages/compass-home/src/components/home.tsx +++ b/packages/compass-home/src/components/home.tsx @@ -44,9 +44,8 @@ import { CreateViewPlugin } from '@mongodb-js/compass-aggregations'; import { CompassFindInPagePlugin } from '@mongodb-js/compass-find-in-page'; import { CreateDatabasePlugin, - DropDatabasePlugin, CreateCollectionPlugin, - DropCollectionPlugin, + DropNamespacePlugin, } from '@mongodb-js/compass-databases-collections'; import { ImportPlugin, ExportPlugin } from '@mongodb-js/compass-import-export'; import { DataServiceProvider } from 'mongodb-data-service/provider'; @@ -360,6 +359,7 @@ function Home({ + @@ -387,9 +387,7 @@ function Home({ - - ); diff --git a/packages/databases-collections/package.json b/packages/databases-collections/package.json index aa9aaaec97d..9aac2289f57 100644 --- a/packages/databases-collections/package.json +++ b/packages/databases-collections/package.json @@ -53,6 +53,7 @@ "@mongodb-js/databases-collections-list": "^1.19.1", "compass-preferences-model": "^2.15.6", "hadron-app-registry": "^9.0.14", + "mongodb-data-service": "^22.15.1", "react": "^17.0.2" }, "devDependencies": { @@ -73,7 +74,6 @@ "mocha": "^10.2.0", "mongodb": "^6.0.0", "mongodb-collection-model": "^5.15.1", - "mongodb-data-service": "^22.15.1", "mongodb-instance-model": "^12.15.1", "mongodb-ns": "^2.4.0", "mongodb-query-parser": "^3.1.3", @@ -92,6 +92,7 @@ "@mongodb-js/compass-logging": "^1.2.6", "@mongodb-js/databases-collections-list": "^1.19.1", "compass-preferences-model": "^2.15.6", - "hadron-app-registry": "^9.0.14" + "hadron-app-registry": "^9.0.14", + "mongodb-data-service": "^22.15.1" } } diff --git a/packages/databases-collections/src/components/drop-collection-modal.tsx b/packages/databases-collections/src/components/drop-collection-modal.tsx deleted file mode 100644 index 880c8d44a04..00000000000 --- a/packages/databases-collections/src/components/drop-collection-modal.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React, { useCallback, useState } from 'react'; -import { connect } from 'react-redux'; -import { - Banner, - Body, - css, - FormFieldContainer, - FormModal, - spacing, - SpinLoader, - TextInput, -} from '@mongodb-js/compass-components'; - -import { dropCollection } from '../modules/drop-collection/drop-collection'; -import { toggleIsVisible } from '../modules/is-visible'; -import type { RootState } from '../modules/drop-collection/drop-collection'; -import { useTrackOnChange } from '@mongodb-js/compass-logging/provider'; - -const progressContainerStyles = css({ - display: 'flex', - gap: spacing[2], - alignItems: 'center', -}); - -type DropCollectionModalProps = { - isRunning: boolean; - isVisible: boolean; - name: string; - error: Error | null; - dropCollection: () => void; - toggleIsVisible: (isVisible: boolean) => void; -}; - -function DropCollectionModal({ - isRunning, - isVisible, - name, - error, - dropCollection, - toggleIsVisible, -}: DropCollectionModalProps) { - const [nameConfirmation, changeCollectionNameConfirmation] = useState(''); - const onNameConfirmationChange = useCallback( - (evt: React.ChangeEvent) => { - changeCollectionNameConfirmation(evt.target.value); - }, - [changeCollectionNameConfirmation] - ); - - const onHide = useCallback(() => { - toggleIsVisible(false); - }, [toggleIsVisible]); - - const onFormSubmit = useCallback(() => { - if (name === nameConfirmation) { - dropCollection(); - changeCollectionNameConfirmation(''); - } - }, [name, nameConfirmation, dropCollection]); - - useTrackOnChange( - 'COMPASS-DATABASES-COLLECTIONS-UI', - (track) => { - if (isVisible) { - track('Screen', { name: 'drop_collection_modal' }); - } - }, - [isVisible], - undefined - ); - - return ( - - - - - {error && {error.message}} - {isRunning && ( - - - Dropping Collection… - - )} - - ); -} - -/** - * Map the store state to properties to pass to the components. - */ -const mapStateToProps = (state: RootState) => ({ - isRunning: state.isRunning, - isVisible: state.isVisible, - name: state.name, - error: state.error, -}); - -/** - * Connect the redux store to the component. - * (dispatch) - */ -const MappedDropCollectionModal = connect(mapStateToProps, { - dropCollection, - toggleIsVisible, -})(DropCollectionModal); - -export default MappedDropCollectionModal; -export { DropCollectionModal }; diff --git a/packages/databases-collections/src/components/drop-database-modal.tsx b/packages/databases-collections/src/components/drop-database-modal.tsx deleted file mode 100644 index 6292cd07961..00000000000 --- a/packages/databases-collections/src/components/drop-database-modal.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import React, { useCallback, useState } from 'react'; -import { connect } from 'react-redux'; -import { - Banner, - Body, - css, - FormFieldContainer, - FormModal, - spacing, - SpinLoader, - TextInput, -} from '@mongodb-js/compass-components'; - -import { dropDatabase } from '../modules/drop-database/drop-database'; -import { toggleIsVisible } from '../modules/is-visible'; -import type { RootState } from '../modules/drop-database/drop-database'; -import { useTrackOnChange } from '@mongodb-js/compass-logging/provider'; - -const progressContainerStyles = css({ - display: 'flex', - gap: spacing[2], - alignItems: 'center', -}); - -type DropDatabaseModalProps = { - isRunning: boolean; - isVisible: boolean; - name: string; - error: Error | null; - dropDatabase: () => void; - toggleIsVisible: (isVisible: boolean) => void; -}; - -/** - * The modal to drop a database. - */ -function DropDatabaseModal({ - isRunning, - isVisible, - name, - error, - dropDatabase, - toggleIsVisible, -}: DropDatabaseModalProps) { - const [nameConfirmation, changeDatabaseNameConfirmation] = useState(''); - const onNameConfirmationChange = useCallback( - (evt: React.ChangeEvent) => { - changeDatabaseNameConfirmation(evt.target.value); - }, - [changeDatabaseNameConfirmation] - ); - - const onHide = useCallback(() => { - toggleIsVisible(false); - }, [toggleIsVisible]); - - const onFormSubmit = useCallback(() => { - if (name === nameConfirmation) { - dropDatabase(); - } - }, [name, nameConfirmation, dropDatabase]); - - useTrackOnChange( - 'COMPASS-DATABASES-COLLECTIONS-UI', - (track) => { - if (isVisible) { - track('Screen', { name: 'drop_database_modal' }); - } - }, - [isVisible], - undefined - ); - - return ( - - - - - {error && {error.message}} - {isRunning && ( - - - Dropping Database… - - )} - - ); -} - -/** - * Map the store state to properties to pass to the components. - */ -const mapStateToProps = (state: RootState) => ({ - isRunning: state.isRunning, - isVisible: state.isVisible, - name: state.name, - error: state.error, -}); - -/** - * Connect the redux store to the component. - * (dispatch) - */ -const MappedDropDatabaseModal = connect(mapStateToProps, { - dropDatabase, - toggleIsVisible, -})(DropDatabaseModal); - -export default MappedDropDatabaseModal; -export { DropDatabaseModal }; diff --git a/packages/databases-collections/src/index.ts b/packages/databases-collections/src/index.ts index f495f144706..d0320aa0231 100644 --- a/packages/databases-collections/src/index.ts +++ b/packages/databases-collections/src/index.ts @@ -7,12 +7,14 @@ import CollectionsStore from './stores/collections-store'; import DatabasesStore from './stores/databases-store'; import CreateDatabaseModal from './components/create-database-modal'; import { activatePlugin as activateCreateDatabasePlugin } from './stores/create-database'; -import DropDatabaseModal from './components/drop-database-modal'; -import { activatePlugin as activateDropDatabasePlugin } from './stores/drop-database'; -import DropCollectionModal from './components/drop-collection-modal'; -import { activatePlugin as activateDropCollectionPlugin } from './stores/drop-collection'; import CreateCollectionModal from './components/create-collection-modal'; import { activatePlugin as activateCreateCollectionPlugin } from './stores/create-collection'; +import { + DropNamespaceComponent, + activatePlugin as activateDropNamespacePlugin, +} from './stores/drop-namespace'; +import { createLoggerAndTelemetryLocator } from '@mongodb-js/compass-logging/provider'; +import { dataServiceLocator } from 'mongodb-data-service/provider'; // View collections list plugin. const COLLECTIONS_PLUGIN_ROLE = { @@ -35,13 +37,6 @@ export const CreateCollectionPlugin = registerHadronPlugin({ activate: activateCreateCollectionPlugin, }); -// Drop collection modal plugin. -export const DropCollectionPlugin = registerHadronPlugin({ - name: 'DropCollection', - component: DropCollectionModal, - activate: activateDropCollectionPlugin, -}); - // Create database modal plugin. export const CreateDatabasePlugin = registerHadronPlugin({ name: 'CreateDatabase', @@ -51,12 +46,19 @@ export const CreateDatabasePlugin = registerHadronPlugin({ activate: activateCreateDatabasePlugin, }); -// Drop database modal plugin. -export const DropDatabasePlugin = registerHadronPlugin({ - name: 'DropDatabase', - component: DropDatabaseModal, - activate: activateDropDatabasePlugin, -}); +export const DropNamespacePlugin = registerHadronPlugin( + { + name: 'DropNamespacePlugin', + component: DropNamespaceComponent, + activate: activateDropNamespacePlugin, + }, + { + logger: createLoggerAndTelemetryLocator('COMPASS-DROP-NAMESPACE-UI'), + dataService: dataServiceLocator as typeof dataServiceLocator< + 'dropDatabase' | 'dropCollection' + >, + } +); /** * Activate all the components in the package. diff --git a/packages/databases-collections/src/modules/drop-collection/drop-collection.spec.js b/packages/databases-collections/src/modules/drop-collection/drop-collection.spec.js deleted file mode 100644 index f0cc7a27062..00000000000 --- a/packages/databases-collections/src/modules/drop-collection/drop-collection.spec.js +++ /dev/null @@ -1,34 +0,0 @@ -import { expect } from 'chai'; -import reducer from './drop-collection'; -import { reset } from '../reset'; - -describe('drop collection module', function () { - describe('#reducer', function () { - context('when an action is provided', function () { - context('when the action is reset', function () { - const dataService = 'data-service'; - - it('returns the reset state', function () { - expect(reducer({ dataService: dataService }, reset())).to.deep.equal({ - isRunning: false, - isVisible: false, - databaseName: '', - name: '', - error: null, - dataService: 'data-service', - }); - }); - }); - }); - }); - - describe('#dropCollection', function () { - context('when an error exists in the state', function () {}); - - context('when no error exists in the state', function () { - context('when the drop is a success', function () {}); - - context('when the drop errors', function () {}); - }); - }); -}); diff --git a/packages/databases-collections/src/modules/drop-collection/drop-collection.ts b/packages/databases-collections/src/modules/drop-collection/drop-collection.ts deleted file mode 100644 index e6aea3e8a96..00000000000 --- a/packages/databases-collections/src/modules/drop-collection/drop-collection.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { combineReducers } from 'redux'; -import type { AnyAction } from 'redux'; -import type { ThunkAction } from 'redux-thunk'; -import isRunning, { - toggleIsRunning, - INITIAL_STATE as IS_RUNNING_INITIAL_STATE, -} from '../is-running'; -import isVisible, { - INITIAL_STATE as IS_VISIBLE_INITIAL_STATE, -} from '../is-visible'; -import name, { INITIAL_STATE as NAME_INITIAL_STATE } from './name'; -import databaseName, { - INITIAL_STATE as DATABASE_NAME_INITIAL_STATE, -} from '../database-name'; -import error, { - clearError, - handleError, - INITIAL_STATE as ERROR_INITIAL_STATE, -} from '../error'; -import { reset, RESET } from '../reset'; -import dataService from '../data-service'; -import appRegistry from '../app-registry'; - -/** - * Open action name. - */ -const OPEN = 'databases-collections/drop-collection/OPEN'; - -const reducer = combineReducers({ - appRegistry, - isRunning, - isVisible, - name, - databaseName, - error, - dataService, -}); - -export type RootState = ReturnType; - -/** - * The root reducer. - */ -const rootReducer = (state: RootState, action: AnyAction): RootState => { - if (action.type === RESET) { - return { - ...state, - isRunning: IS_RUNNING_INITIAL_STATE, - isVisible: IS_VISIBLE_INITIAL_STATE, - name: NAME_INITIAL_STATE, - databaseName: DATABASE_NAME_INITIAL_STATE, - error: ERROR_INITIAL_STATE, - }; - } else if (action.type === OPEN) { - return { - ...state, - isVisible: true, - name: action.collectionName, - databaseName: action.databaseName, - isRunning: IS_RUNNING_INITIAL_STATE, - error: ERROR_INITIAL_STATE, - }; - } - return reducer(state, action); -}; - -export default rootReducer; - -/** - * Open create collection action creator. - */ -export const open = (collectionName: string, dbName: string) => ({ - type: OPEN, - collectionName: collectionName, - databaseName: dbName, -}); - -/** - * The drop collection action. - */ -export const dropCollection = (): ThunkAction< - Promise, - RootState, - void, - AnyAction -> => { - return async (dispatch, getState) => { - const state = getState(); - const ds = state.dataService.dataService; - const collectionName = state.name; - const dbName = state.databaseName; - - dispatch(clearError()); - - if (!ds) { - return; - } - - try { - dispatch(toggleIsRunning(true)); - const namespace = `${dbName}.${collectionName}`; - await ds.dropCollection(namespace); - const { appRegistry } = getState(); - appRegistry?.emit('collection-dropped', namespace); - dispatch(reset()); - } catch (e) { - dispatch(toggleIsRunning(false)); - dispatch(handleError(e as Error)); - } - }; -}; diff --git a/packages/databases-collections/src/modules/drop-collection/name.ts b/packages/databases-collections/src/modules/drop-collection/name.ts deleted file mode 100644 index 281e69ccbe8..00000000000 --- a/packages/databases-collections/src/modules/drop-collection/name.ts +++ /dev/null @@ -1,7 +0,0 @@ -type State = string; - -export const INITIAL_STATE: State = ''; - -export default function reducer(state = INITIAL_STATE): State { - return state; -} diff --git a/packages/databases-collections/src/modules/drop-database/drop-database.spec.js b/packages/databases-collections/src/modules/drop-database/drop-database.spec.js deleted file mode 100644 index c5d0dbde792..00000000000 --- a/packages/databases-collections/src/modules/drop-database/drop-database.spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import { expect } from 'chai'; -import reducer from './drop-database'; -import { reset } from '../reset'; - -describe('drop database module', function () { - describe('#reducer', function () { - context('when an action is provided', function () { - context('when the action is reset', function () { - const dataService = 'data-service'; - - it('returns the reset state', function () { - expect(reducer({ dataService: dataService }, reset())).to.deep.equal({ - isRunning: false, - isVisible: false, - name: '', - error: null, - dataService: 'data-service', - }); - }); - }); - }); - }); - - describe('#dropDatabase', function () { - context('when an error exists in the state', function () {}); - - context('when no error exists in the state', function () { - context('when the drop is a success', function () {}); - - context('when the drop errors', function () {}); - }); - }); -}); diff --git a/packages/databases-collections/src/modules/drop-database/drop-database.ts b/packages/databases-collections/src/modules/drop-database/drop-database.ts deleted file mode 100644 index 22ea98238ec..00000000000 --- a/packages/databases-collections/src/modules/drop-database/drop-database.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { combineReducers } from 'redux'; -import type { AnyAction } from 'redux'; -import type { ThunkAction, ThunkDispatch } from 'redux-thunk'; - -import isRunning, { - toggleIsRunning, - INITIAL_STATE as IS_RUNNING_INITIAL_STATE, -} from '../is-running'; -import isVisible, { - INITIAL_STATE as IS_VISIBLE_INITIAL_STATE, -} from '../is-visible'; -import error, { - clearError, - handleError, - INITIAL_STATE as ERROR_INITIAL_STATE, -} from '../error'; -import { reset, RESET } from '../reset'; -import dataService from '../data-service'; -import name, { INITIAL_STATE as NAME_INITIAL_STATE } from './name'; -import appRegistry from '../app-registry'; - -/** - * Open action name. - */ -const OPEN = 'databases-collections/drop-database/OPEN'; - -/** - * The main reducer. - */ -const reducer = combineReducers({ - isRunning, - isVisible, - name, - error, - dataService, - appRegistry, -}); - -export type RootState = ReturnType; - -/** - * The root reducer. - */ -const rootReducer = (state: RootState, action: AnyAction): RootState => { - if (action.type === RESET) { - return { - ...state, - isRunning: IS_RUNNING_INITIAL_STATE, - isVisible: IS_VISIBLE_INITIAL_STATE, - name: NAME_INITIAL_STATE, - error: ERROR_INITIAL_STATE, - }; - } else if (action.type === OPEN) { - return { - ...state, - isVisible: true, - name: action.name, - isRunning: IS_RUNNING_INITIAL_STATE, - error: ERROR_INITIAL_STATE, - }; - } - return reducer(state, action); -}; - -export default rootReducer; - -/** - * Open drop database action creator. - */ -export const open = (dbName: string) => ({ - type: OPEN, - name: dbName, -}); - -/** - * The drop database action. - */ -export const dropDatabase = (): ThunkAction< - Promise, - RootState, - void, - AnyAction -> => { - return async ( - dispatch: ThunkDispatch, - getState: () => RootState - ) => { - const state = getState(); - const ds = state.dataService.dataService; - const dbName = state.name; - - dispatch(clearError()); - - if (!ds) { - return; - } - - try { - dispatch(toggleIsRunning(true)); - await ds.dropDatabase(dbName); - const { appRegistry } = getState(); - appRegistry?.emit('database-dropped', dbName); - dispatch(reset()); - } catch (e: any) { - dispatch(toggleIsRunning(false)); - dispatch(handleError(e)); - } - }; -}; diff --git a/packages/databases-collections/src/modules/drop-database/name.ts b/packages/databases-collections/src/modules/drop-database/name.ts deleted file mode 100644 index 281e69ccbe8..00000000000 --- a/packages/databases-collections/src/modules/drop-database/name.ts +++ /dev/null @@ -1,7 +0,0 @@ -type State = string; - -export const INITIAL_STATE: State = ''; - -export default function reducer(state = INITIAL_STATE): State { - return state; -} diff --git a/packages/databases-collections/src/stores/drop-collection.js b/packages/databases-collections/src/stores/drop-collection.js deleted file mode 100644 index f527e599ca8..00000000000 --- a/packages/databases-collections/src/stores/drop-collection.js +++ /dev/null @@ -1,36 +0,0 @@ -import { createStore, applyMiddleware } from 'redux'; -import thunk from 'redux-thunk'; -import { appRegistryActivated } from '../modules/app-registry'; -import { dataServiceConnected } from '../modules/data-service'; -import reducer, { open } from '../modules/drop-collection/drop-collection'; - -export function activatePlugin(_, { globalAppRegistry }) { - const store = createStore(reducer, applyMiddleware(thunk)); - store.dispatch(appRegistryActivated(globalAppRegistry)); - - const onDataServiceConnected = (error, dataService) => { - store.dispatch(dataServiceConnected(error, dataService)); - }; - - globalAppRegistry.on('data-service-connected', onDataServiceConnected); - - const onOpenDropCollection = ({ database, collection }) => { - store.dispatch(open(collection, database)); - }; - - globalAppRegistry.on('open-drop-collection', onOpenDropCollection); - - return { - store, - deactivate() { - globalAppRegistry.removeListener( - 'data-service-connected', - onDataServiceConnected - ); - globalAppRegistry.removeListener( - 'open-drop-collection', - onOpenDropCollection - ); - }, - }; -} diff --git a/packages/databases-collections/src/stores/drop-collection.spec.js b/packages/databases-collections/src/stores/drop-collection.spec.js deleted file mode 100644 index 918f3ecf386..00000000000 --- a/packages/databases-collections/src/stores/drop-collection.spec.js +++ /dev/null @@ -1,57 +0,0 @@ -import { expect } from 'chai'; -import AppRegistry from 'hadron-app-registry'; -import { activatePlugin } from './drop-collection'; -import { reset } from '../modules/reset'; - -describe('DropCollectionStore [Store]', function () { - let store; - - beforeEach(function () { - store.dispatch(reset()); - }); - - afterEach(function () { - store.dispatch(reset()); - }); - - describe('#onActivated', function () { - const appRegistry = new AppRegistry(); - - before(function () { - ({ store } = activatePlugin({}, { globalAppRegistry: appRegistry })); - }); - - context('when the data service is connected', function () { - const ds = { _testId: 'data-service', on() {} }; - - beforeEach(function () { - appRegistry.emit('data-service-connected', null, ds); - }); - - it('dispatches the data service connected action', function () { - expect(store.getState().dataService.dataService).to.equal(ds); - }); - }); - - context('when open drop collection is emitted', function () { - beforeEach(function () { - appRegistry.emit('open-drop-collection', { - database: 'testing', - collection: 'test', - }); - }); - - it('dispatches the toggle action', function () { - expect(store.getState().isVisible).to.equal(true); - }); - - it('sets the name in the store', function () { - expect(store.getState().name).to.equal('test'); - }); - - it('sets the database name in the store', function () { - expect(store.getState().databaseName).to.equal('testing'); - }); - }); - }); -}); diff --git a/packages/databases-collections/src/stores/drop-database.js b/packages/databases-collections/src/stores/drop-database.js deleted file mode 100644 index e8629f8adf2..00000000000 --- a/packages/databases-collections/src/stores/drop-database.js +++ /dev/null @@ -1,36 +0,0 @@ -import { createStore, applyMiddleware } from 'redux'; -import thunk from 'redux-thunk'; -import { appRegistryActivated } from '../modules/app-registry'; -import { dataServiceConnected } from '../modules/data-service'; -import reducer, { open } from '../modules/drop-database/drop-database'; - -export function activatePlugin(_, { globalAppRegistry }) { - const store = createStore(reducer, applyMiddleware(thunk)); - store.dispatch(appRegistryActivated(globalAppRegistry)); - - const onDataServiceConnected = (error, dataService) => { - store.dispatch(dataServiceConnected(error, dataService)); - }; - - globalAppRegistry.on('data-service-connected', onDataServiceConnected); - - const onOpenDropDatabase = (name) => { - store.dispatch(open(name)); - }; - - globalAppRegistry.on('open-drop-database', onOpenDropDatabase); - - return { - store, - deactivate() { - globalAppRegistry.removeListener( - 'data-service-connected', - onDataServiceConnected - ); - globalAppRegistry.removeListener( - 'open-drop-database', - onOpenDropDatabase - ); - }, - }; -} diff --git a/packages/databases-collections/src/stores/drop-database.spec.js b/packages/databases-collections/src/stores/drop-database.spec.js deleted file mode 100644 index 26a9ffd3e72..00000000000 --- a/packages/databases-collections/src/stores/drop-database.spec.js +++ /dev/null @@ -1,50 +0,0 @@ -import { expect } from 'chai'; -import AppRegistry from 'hadron-app-registry'; -import { activatePlugin } from './drop-database'; -import { reset } from '../modules/reset'; - -describe('DropDatabaseStore [Store]', function () { - let store; - - beforeEach(function () { - store.dispatch(reset()); - }); - - afterEach(function () { - store.dispatch(reset()); - }); - - describe('#onActivated', function () { - const appRegistry = new AppRegistry(); - - before(function () { - ({ store } = activatePlugin({}, { globalAppRegistry: appRegistry })); - }); - - context('when the data service is connected', function () { - const ds = { _testId: 'data-service', on() {} }; - - beforeEach(function () { - appRegistry.emit('data-service-connected', null, ds); - }); - - it('dispatches the data service connected action', function () { - expect(store.getState().dataService.dataService).to.equal(ds); - }); - }); - - context('when open drop database is emitted', function () { - beforeEach(function () { - appRegistry.emit('open-drop-database', 'testing'); - }); - - it('dispatches the toggle action', function () { - expect(store.getState().isVisible).to.equal(true); - }); - - it('sets the name in the store', function () { - expect(store.getState().name).to.equal('testing'); - }); - }); - }); -}); diff --git a/packages/databases-collections/src/stores/drop-namespace.spec.tsx b/packages/databases-collections/src/stores/drop-namespace.spec.tsx new file mode 100644 index 00000000000..7fcc384f5c8 --- /dev/null +++ b/packages/databases-collections/src/stores/drop-namespace.spec.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import Sinon from 'sinon'; +import { DropNamespaceComponent, activatePlugin } from './drop-namespace'; +import AppRegistry from 'hadron-app-registry'; +import toNS from 'mongodb-ns'; +import { render, cleanup, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { expect } from 'chai'; + +describe('DropNamespacePlugin', function () { + const sandbox = Sinon.createSandbox(); + const appRegistry = sandbox.spy(new AppRegistry()); + const dataService = { + dropDatabase: sandbox.stub().resolves(true), + dropCollection: sandbox.stub().resolves(true), + }; + const logger = { track: sandbox.stub() }; + + beforeEach(function () { + render(); + activatePlugin( + {}, + { globalAppRegistry: appRegistry, dataService, logger: logger as any } + ); + }); + + afterEach(function () { + sandbox.resetHistory(); + appRegistry.deactivate(); + cleanup(); + }); + + it('should ask for confirmation and delete collection on `open-drop-collection` event', async function () { + appRegistry.emit('open-drop-collection', toNS('test.to-drop')); + + expect( + screen.getByText( + 'Are you sure you want to drop collection "test.to-drop"?' + ) + ).to.exist; + + const input = screen.getByRole('textbox', { + name: `Type "to-drop" to confirm your action`, + }); + + userEvent.type(input, 'to-drop'); + + const dropButton = screen.getByRole('button', { name: 'Drop Collection' }); + + userEvent.click(dropButton); + + await waitFor(() => { + expect(screen.getByText('Collection "test.to-drop" dropped')).to.exist; + }); + + expect(dataService.dropCollection).to.have.been.calledOnceWithExactly( + 'test.to-drop' + ); + expect(dataService.dropDatabase).to.have.not.been.called; + }); + + it('should ask for confirmation and delete database on `open-drop-database` event', async function () { + appRegistry.emit('open-drop-database', 'db-to-drop'); + + expect( + screen.getByText('Are you sure you want to drop database "db-to-drop"?') + ).to.exist; + + const input = screen.getByRole('textbox', { + name: `Type "db-to-drop" to confirm your action`, + }); + + userEvent.type(input, 'db-to-drop'); + + const dropButton = screen.getByRole('button', { name: 'Drop Database' }); + + userEvent.click(dropButton); + + await waitFor(() => { + expect(screen.getByText('Database "db-to-drop" dropped')).to.exist; + }); + + expect(dataService.dropDatabase).to.have.been.calledOnceWithExactly( + 'db-to-drop' + ); + expect(dataService.dropCollection).to.have.not.been.called; + }); +}); diff --git a/packages/databases-collections/src/stores/drop-namespace.tsx b/packages/databases-collections/src/stores/drop-namespace.tsx new file mode 100644 index 00000000000..2380653c5b2 --- /dev/null +++ b/packages/databases-collections/src/stores/drop-namespace.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { + openToast, + showConfirmation, + ConfirmationModalArea, + ToastArea, +} from '@mongodb-js/compass-components'; +import type { LoggerAndTelemetry } from '@mongodb-js/compass-logging/provider'; +import type AppRegistry from 'hadron-app-registry'; +import type { DataService } from 'mongodb-data-service'; +import toNS from 'mongodb-ns'; + +type NS = ReturnType; + +type DropNamespaceServices = { + globalAppRegistry: AppRegistry; + dataService: Pick; + logger: LoggerAndTelemetry; +}; + +export function activatePlugin( + _: unknown, + { globalAppRegistry, dataService, logger: { track } }: DropNamespaceServices +) { + const onDropNamespace = (ns: string | NS) => { + // `drop-collection` is emitted with NS, `drop-database` is emitted with a + // string, we're keeping compat with both for now to avoid conflicts with + // other refactoring + if (typeof ns === 'string') { + ns = toNS(ns); + } + + void (async (namespace: NS) => { + const { + ns, + validCollectionName: isCollection, + database, + collection, + } = namespace; + const namespaceLabel = isCollection ? 'Collection' : 'Database'; + track('Screen', { + name: isCollection ? 'drop_collection_modal' : 'drop_database_modal', + }); + const confirmed = await showConfirmation({ + variant: 'danger', + title: `Drop ${namespaceLabel}`, + description: `Are you sure you want to drop ${namespaceLabel.toLocaleLowerCase()} "${ns}"?`, + requiredInputText: isCollection ? collection : database, + buttonText: `Drop ${namespaceLabel}`, + 'data-testid': 'drop-namespace-confirmation-modal', + }); + if (confirmed) { + try { + const method = isCollection ? 'dropCollection' : 'dropDatabase'; + await dataService[method](ns); + globalAppRegistry.emit( + isCollection ? 'collection-dropped' : 'database-dropped', + ns + ); + openToast('drop-namespace-success', { + variant: 'success', + title: `${namespaceLabel} "${ns}" dropped`, + timeout: 3000, + }); + } catch (err) { + openToast('drop-namespace-error', { + variant: 'important', + title: `Failed to drop ${namespaceLabel.toLocaleLowerCase()} "${ns}"`, + description: (err as Error).message, + timeout: 3000, + }); + } + } + })(ns); + }; + + globalAppRegistry.on('open-drop-database', onDropNamespace); + globalAppRegistry.on('open-drop-collection', onDropNamespace); + + return { + store: {}, + deactivate() { + globalAppRegistry.removeListener('open-drop-database', onDropNamespace); + globalAppRegistry.removeListener('open-drop-collection', onDropNamespace); + }, + }; +} + +/** + * Drop namespace plugin doesn't render anything on it's own, but requires + * compass-component toast and confirmation modal areas to be present + */ +export const DropNamespaceComponent: React.FunctionComponent = ({ + children, +}) => { + return ( + + {children} + + ); +};