diff --git a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-header/pipeline-actions.spec.tsx b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-header/pipeline-actions.spec.tsx
index d9dbde2a2f4..400101b3a8c 100644
--- a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-header/pipeline-actions.spec.tsx
+++ b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-header/pipeline-actions.spec.tsx
@@ -1,12 +1,17 @@
import React from 'react';
-import { cleanup, render, screen, within } from '@testing-library/react';
+import {
+ cleanup,
+ render,
+ screen,
+ waitFor,
+ within,
+} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { expect } from 'chai';
import { spy } from 'sinon';
import type { SinonSpy } from 'sinon';
import ConnectedPipelineActions, { PipelineActions } from './pipeline-actions';
-import configureStore from '../../../../test/configure-store';
-import { Provider } from 'react-redux';
+import { renderWithStore } from '../../../../test/configure-store';
import { changeStageDisabled } from '../../../modules/pipeline-builder/stage-editor';
import {
type PreferencesAccess,
@@ -230,32 +235,24 @@ describe('PipelineActions', function () {
});
describe('with store', function () {
- function renderPipelineActions(options = {}) {
- const store = configureStore(options);
-
- const component = (
-
- {}}
- >
-
+ async function renderPipelineActions(options = {}) {
+ const result = await renderWithStore(
+
{}}
+ > ,
+ options
);
-
- const result = render(component);
return {
...result,
- store,
- rerender: () => {
- result.rerender(component);
- },
+ store: result.plugin.store,
};
}
- it('should disable actions when pipeline contains errors', function () {
- renderPipelineActions({ pipeline: [42] });
+ it('should disable actions when pipeline contains errors', async function () {
+ await renderPipelineActions({ pipeline: [42] });
expect(
screen
@@ -276,8 +273,8 @@ describe('PipelineActions', function () {
).to.equal('true');
});
- it('should disable actions while ai is fetching', function () {
- const { store, rerender } = renderPipelineActions({
+ it('should disable actions while ai is fetching', async function () {
+ const { store } = await renderPipelineActions({
pipeline: [{ $match: { _id: 1 } }],
});
@@ -285,29 +282,30 @@ describe('PipelineActions', function () {
type: AIPipelineActionTypes.AIPipelineStarted,
requestId: 'pineapples',
});
- rerender();
- expect(
- screen
- .getByTestId('pipeline-toolbar-explain-aggregation-button')
- .getAttribute('aria-disabled')
- ).to.equal('true');
-
- expect(
- screen
- .getByTestId('pipeline-toolbar-export-aggregation-button')
- .getAttribute('aria-disabled')
- ).to.equal('true');
-
- expect(
- screen
- .getByTestId('pipeline-toolbar-run-button')
- .getAttribute('aria-disabled')
- ).to.equal('true');
+ await waitFor(() => {
+ expect(
+ screen
+ .getByTestId('pipeline-toolbar-explain-aggregation-button')
+ .getAttribute('aria-disabled')
+ ).to.equal('true');
+
+ expect(
+ screen
+ .getByTestId('pipeline-toolbar-export-aggregation-button')
+ .getAttribute('aria-disabled')
+ ).to.equal('true');
+
+ expect(
+ screen
+ .getByTestId('pipeline-toolbar-run-button')
+ .getAttribute('aria-disabled')
+ ).to.equal('true');
+ });
});
- it('should disable export button when pipeline is $out / $merge', function () {
- renderPipelineActions({
+ it('should disable export button when pipeline is $out / $merge', async function () {
+ await renderPipelineActions({
pipeline: [{ $out: 'foo' }],
});
@@ -318,20 +316,20 @@ describe('PipelineActions', function () {
).to.equal('true');
});
- it('should disable export button when last enabled stage is $out / $merge', function () {
- const { store, rerender } = renderPipelineActions({
+ it('should disable export button when last enabled stage is $out / $merge', async function () {
+ const { store } = await renderPipelineActions({
pipeline: [{ $out: 'foo' }, { $match: { _id: 1 } }],
});
- store.dispatch(changeStageDisabled(1, true) as any);
-
- rerender();
+ store.dispatch(changeStageDisabled(1, true));
- expect(
- screen
- .getByTestId('pipeline-toolbar-export-aggregation-button')
- .getAttribute('aria-disabled')
- ).to.equal('true');
+ await waitFor(() => {
+ expect(
+ screen
+ .getByTestId('pipeline-toolbar-export-aggregation-button')
+ .getAttribute('aria-disabled')
+ ).to.equal('true');
+ });
});
});
});
diff --git a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-options/index.spec.tsx b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-options/index.spec.tsx
index 7ba0bacf743..823411c0254 100644
--- a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-options/index.spec.tsx
+++ b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-options/index.spec.tsx
@@ -1,19 +1,14 @@
import React from 'react';
-import { render, screen, within } from '@testing-library/react';
+import { screen, within } from '@testing-library/react';
import { expect } from 'chai';
-import { Provider } from 'react-redux';
-import configureStore from '../../../../test/configure-store';
+import { renderWithStore } from '../../../../test/configure-store';
import { PipelineOptions } from '.';
describe('PipelineOptions', function () {
let container: HTMLElement;
- beforeEach(function () {
- render(
-
-
-
- );
+ beforeEach(async function () {
+ await renderWithStore(
);
container = screen.getByTestId('pipeline-options');
});
diff --git a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-options/pipeline-collation.spec.tsx b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-options/pipeline-collation.spec.tsx
index 7de675f1799..b1f1ade7bdf 100644
--- a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-options/pipeline-collation.spec.tsx
+++ b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-options/pipeline-collation.spec.tsx
@@ -1,21 +1,17 @@
import React from 'react';
-import { render, screen } from '@testing-library/react';
+import { screen } from '@testing-library/react';
import { expect } from 'chai';
-import { Provider } from 'react-redux';
import userEvent from '@testing-library/user-event';
-import configureStore from '../../../../test/configure-store';
+import { renderWithStore } from '../../../../test/configure-store';
import PipelineCollation from './pipeline-collation';
+import type { AggregationsStore } from '../../../stores/store';
describe('PipelineCollation', function () {
- let store: ReturnType
;
- beforeEach(function () {
- store = configureStore();
- render(
-
-
-
- );
+ let store: AggregationsStore;
+ beforeEach(async function () {
+ const result = await renderWithStore( );
+ store = result.plugin.store;
});
it('renders the collation toolbar', function () {
diff --git a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-settings/index.spec.tsx b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-settings/index.spec.tsx
index 241b43e87eb..a00405948ae 100644
--- a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-settings/index.spec.tsx
+++ b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-settings/index.spec.tsx
@@ -1,29 +1,26 @@
import React from 'react';
-import { cleanup, render, screen, within } from '@testing-library/react';
+import { cleanup, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { expect } from 'chai';
import { spy } from 'sinon';
import type { SinonSpy } from 'sinon';
-import { Provider } from 'react-redux';
-import configureStore from '../../../../test/configure-store';
+import { renderWithStore } from '../../../../test/configure-store';
import { PipelineSettings } from '.';
describe('PipelineSettings', function () {
let container: HTMLElement;
let onExportToLanguageSpy: SinonSpy;
let onCreateNewPipelineSpy: SinonSpy;
- beforeEach(function () {
+ beforeEach(async function () {
onExportToLanguageSpy = spy();
onCreateNewPipelineSpy = spy();
- render(
-
-
-
+ await renderWithStore(
+
);
container = screen.getByTestId('pipeline-settings');
});
diff --git a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-settings/index.tsx b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-settings/index.tsx
index 2a383811548..04458b965ff 100644
--- a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-settings/index.tsx
+++ b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-settings/index.tsx
@@ -47,6 +47,8 @@ export const PipelineSettings: React.FunctionComponent<
onExportToLanguage,
onCreateNewPipeline,
}) => {
+ // TODO: remove direct check for storage existing, breaks single source of
+ // truth rule and exposes services to UI, this breaks the rules for locators
const enableSavedAggregationsQueries = !!usePipelineStorage();
const isPipelineNameDisplayed =
!editViewName && !!enableSavedAggregationsQueries;
diff --git a/packages/compass-aggregations/src/components/stage-preview/index.spec.tsx b/packages/compass-aggregations/src/components/stage-preview/index.spec.tsx
index b80fe2f44bf..e05305a3a1f 100644
--- a/packages/compass-aggregations/src/components/stage-preview/index.spec.tsx
+++ b/packages/compass-aggregations/src/components/stage-preview/index.spec.tsx
@@ -1,11 +1,10 @@
import React from 'react';
import type { ComponentProps } from 'react';
import type { Document } from 'mongodb';
-import { render, screen, cleanup } from '@testing-library/react';
+import { screen, cleanup } from '@testing-library/react';
import { expect } from 'chai';
-import { Provider } from 'react-redux';
-import configureStore from '../../../test/configure-store';
+import { renderWithStore } from '../../../test/configure-store';
import { StagePreview } from './';
import {
@@ -19,50 +18,45 @@ const renderStagePreview = (
props: Partial> = {},
pipeline = DEFAULT_PIPELINE
) => {
- render(
-
-
-
+ return renderWithStore(
+ ,
+ { pipeline }
);
};
describe('StagePreview', function () {
afterEach(cleanup);
- it('renders empty content when stage is disabled', function () {
- renderStagePreview({
+ it('renders empty content when stage is disabled', async function () {
+ await renderStagePreview({
isDisabled: true,
});
expect(screen.getByTestId('stage-preview-empty')).to.exist;
});
- it('renders no preview documents when stage can not be previewed', function () {
- renderStagePreview({
+ it('renders no preview documents when stage can not be previewed', async function () {
+ await renderStagePreview({
shouldRenderStage: false,
});
expect(screen.getByTestId('stage-preview-empty')).to.exist;
});
- it('renders atlas preview when operator is $search', function () {
- renderStagePreview({
+ it('renders atlas preview when operator is $search', async function () {
+ await renderStagePreview({
shouldRenderStage: true,
isMissingAtlasOnlyStageSupport: true,
stageOperator: '$search',
});
expect(screen.getByTestId('atlas-only-stage-preview')).to.exist;
});
- it('renders out preivew when operator is $out', function () {
- renderStagePreview(
+ it('renders out preivew when operator is $out', async function () {
+ await renderStagePreview(
{
shouldRenderStage: true,
stageOperator: '$out',
@@ -72,8 +66,8 @@ describe('StagePreview', function () {
);
expect(screen.getByText(OUT_STAGE_PREVIEW_TEXT)).to.exist;
});
- it('renders merge preview when operator is $merge', function () {
- renderStagePreview(
+ it('renders merge preview when operator is $merge', async function () {
+ await renderStagePreview(
{
shouldRenderStage: true,
stageOperator: '$merge',
@@ -83,31 +77,31 @@ describe('StagePreview', function () {
);
expect(screen.getByText(MERGE_STAGE_PREVIEW_TEXT)).to.exist;
});
- it('renders loading preview docs', function () {
- renderStagePreview({
+ it('renders loading preview docs', async function () {
+ await renderStagePreview({
shouldRenderStage: true,
isLoading: true,
stageOperator: '$match',
});
expect(screen.getByText(/Loading Preview Documents.../i)).to.exist;
});
- it('renders no preview documents when there are no documents', function () {
- renderStagePreview({
+ it('renders no preview documents when there are no documents', async function () {
+ await renderStagePreview({
shouldRenderStage: true,
documents: [],
});
expect(screen.getByTestId('stage-preview-empty')).to.exist;
});
- it('renders list of documents', function () {
- renderStagePreview({
+ it('renders list of documents', async function () {
+ await renderStagePreview({
shouldRenderStage: true,
documents: [{ _id: 1 }, { _id: 2 }],
});
const docs = screen.getAllByTestId('readonly-document');
expect(docs).to.have.length(2);
});
- it('renders missing search index text for $search', function () {
- renderStagePreview({
+ it('renders missing search index text for $search', async function () {
+ await renderStagePreview({
shouldRenderStage: true,
stageOperator: '$search',
documents: [],
@@ -119,8 +113,8 @@ describe('StagePreview', function () {
)
).to.exist;
});
- it('renders $search preview docs', function () {
- renderStagePreview({
+ it('renders $search preview docs', async function () {
+ await renderStagePreview({
shouldRenderStage: true,
stageOperator: '$search',
documents: [{ _id: 1 }, { _id: 2 }],
diff --git a/packages/compass-aggregations/src/components/stage-toolbar/index.spec.tsx b/packages/compass-aggregations/src/components/stage-toolbar/index.spec.tsx
index 9f19ca12532..a374637faa9 100644
--- a/packages/compass-aggregations/src/components/stage-toolbar/index.spec.tsx
+++ b/packages/compass-aggregations/src/components/stage-toolbar/index.spec.tsx
@@ -1,54 +1,48 @@
import React from 'react';
-import { render, screen } from '@testing-library/react';
+import { screen } from '@testing-library/react';
import { expect } from 'chai';
-import { Provider } from 'react-redux';
-import configureStore from '../../../test/configure-store';
+import { renderWithStore } from '../../../test/configure-store';
import StageToolbar from './';
import {
changeStageCollapsed,
changeStageDisabled,
} from '../../modules/pipeline-builder/stage-editor';
-const renderStageToolbar = () => {
- const store = configureStore({
+const renderStageToolbar = async () => {
+ const result = await renderWithStore( , {
pipeline: [{ $match: { _id: 1 } }, { $limit: 10 }, { $out: 'out' }],
});
- render(
-
-
-
- );
- return store;
+ return result.plugin.store;
};
describe('StageToolbar', function () {
- it('renders collapse button', function () {
- renderStageToolbar();
+ it('renders collapse button', async function () {
+ await renderStageToolbar();
expect(screen.getByLabelText('Collapse')).to.exist;
});
- it('renders stage number text', function () {
- renderStageToolbar();
+ it('renders stage number text', async function () {
+ await renderStageToolbar();
expect(screen.getByText('Stage 1')).to.exist;
});
- it('render stage operator select', function () {
- renderStageToolbar();
+ it('render stage operator select', async function () {
+ await renderStageToolbar();
expect(screen.getByTestId('stage-operator-combobox')).to.exist;
});
- it('renders stage enable/disable toggle', function () {
- renderStageToolbar();
+ it('renders stage enable/disable toggle', async function () {
+ await renderStageToolbar();
expect(screen.getByLabelText('Exclude stage from pipeline')).to.exist;
});
context('renders stage text', function () {
- it('when stage is disabled', function () {
- const store = renderStageToolbar();
+ it('when stage is disabled', async function () {
+ const store = await renderStageToolbar();
store.dispatch(changeStageDisabled(0, true));
expect(
screen.getByText('Stage disabled. Results not passed in the pipeline.')
).to.exist;
});
- it('when stage is collapsed', function () {
- const store = renderStageToolbar();
+ it('when stage is collapsed', async function () {
+ const store = await renderStageToolbar();
store.dispatch(changeStageCollapsed(0, true));
expect(
screen.getByText(
@@ -57,8 +51,8 @@ describe('StageToolbar', function () {
).to.exist;
});
});
- it('renders option menu', function () {
- renderStageToolbar();
+ it('renders option menu', async function () {
+ await renderStageToolbar();
expect(screen.getByTestId('stage-option-menu-button')).to.exist;
});
});
diff --git a/packages/compass-aggregations/src/modules/aggregation.spec.ts b/packages/compass-aggregations/src/modules/aggregation.spec.ts
index 7414126a540..5f1bdf29bf3 100644
--- a/packages/compass-aggregations/src/modules/aggregation.spec.ts
+++ b/packages/compass-aggregations/src/modules/aggregation.spec.ts
@@ -23,6 +23,7 @@ import { EJSON } from 'bson';
import { defaultPreferencesInstance } from 'compass-preferences-model';
import { createNoopLogger } from '@mongodb-js/compass-logging/provider';
import { createNoopTrack } from '@mongodb-js/compass-telemetry/provider';
+import type { AggregationsStore } from '../stores/store';
const getMockedStore = (
aggregation: AggregateState,
@@ -68,14 +69,16 @@ describe('aggregation module', function () {
it('runs an aggregation', async function () {
const mockDocuments = [{ id: 1 }, { id: 2 }];
- const store: Store = configureStore(
- { pipeline: [] },
- new (class {
- aggregate() {
- return Promise.resolve(mockDocuments);
+ const store: AggregationsStore = (
+ await configureStore(
+ { pipeline: [] },
+ {
+ aggregate() {
+ return Promise.resolve(mockDocuments);
+ },
}
- })() as any
- );
+ )
+ ).plugin.store;
await store.dispatch(runAggregation() as any);
const aggregation = store.getState().aggregation;
@@ -111,14 +114,14 @@ describe('aggregation module', function () {
page: 2,
resultsViewType: 'document',
},
- new (class {
+ {
aggregate() {
throw createCancelError();
- }
+ },
isCancelError() {
return true;
- }
- })() as any
+ },
+ } as any
);
store.dispatch(fetchNextPage() as any);
@@ -153,11 +156,11 @@ describe('aggregation module', function () {
page: 2,
resultsViewType: 'document',
},
- new (class {
+ {
aggregate() {
return Promise.resolve(mockDocuments);
- }
- })() as any
+ },
+ } as any
);
await store.dispatch(fetchNextPage() as any);
@@ -214,11 +217,11 @@ describe('aggregation module', function () {
page: 2,
resultsViewType: 'document',
},
- new (class {
+ {
aggregate() {
return Promise.resolve(mockDocuments);
- }
- })() as any
+ },
+ } as any
);
await store.dispatch(fetchPrevPage() as any);
diff --git a/packages/compass-aggregations/src/modules/auto-preview.spec.ts b/packages/compass-aggregations/src/modules/auto-preview.spec.ts
index e3ae538821d..a8384833742 100644
--- a/packages/compass-aggregations/src/modules/auto-preview.spec.ts
+++ b/packages/compass-aggregations/src/modules/auto-preview.spec.ts
@@ -1,11 +1,12 @@
import { expect } from 'chai';
import { toggleAutoPreview } from './auto-preview';
import configureStore from '../../test/configure-store';
+import type { AggregationsStore } from '../stores/store';
describe('auto preview module', function () {
- let store: ReturnType;
- beforeEach(function () {
- store = configureStore();
+ let store: AggregationsStore;
+ beforeEach(async function () {
+ store = (await configureStore()).plugin.store;
});
it('returns the default state', function () {
diff --git a/packages/compass-aggregations/src/modules/collections-fields.spec.ts b/packages/compass-aggregations/src/modules/collections-fields.spec.ts
index 545d95deca9..3826344302f 100644
--- a/packages/compass-aggregations/src/modules/collections-fields.spec.ts
+++ b/packages/compass-aggregations/src/modules/collections-fields.spec.ts
@@ -1,5 +1,3 @@
-import type { AnyAction, Store } from 'redux';
-import type { RootState } from '.';
import type { Document } from 'mongodb';
import { expect } from 'chai';
import reducer, {
@@ -10,6 +8,7 @@ import reducer, {
} from './collections-fields';
import configureStore from '../../test/configure-store';
import sinon from 'sinon';
+import type { AggregationsStore } from '../stores/store';
describe('collections-fields module', function () {
describe('#reducer', function () {
@@ -70,18 +69,20 @@ describe('collections-fields module', function () {
});
});
describe('#actions', function () {
- let store: Store;
+ let store: AggregationsStore;
let sampleStub: sinon.SinonStub;
let findStub: sinon.SinonStub;
let sandbox: sinon.SinonSandbox;
- beforeEach(function () {
+ beforeEach(async function () {
sandbox = sinon.createSandbox();
sampleStub = sandbox.stub();
findStub = sandbox.stub();
- store = configureStore({ pipeline: [] }, {
- sample: sampleStub,
- find: findStub,
- } as any);
+ store = (
+ await configureStore({ pipeline: [] }, {
+ sample: sampleStub,
+ find: findStub,
+ } as any)
+ ).plugin.store;
});
afterEach(function () {
diff --git a/packages/compass-aggregations/src/modules/insights.spec.ts b/packages/compass-aggregations/src/modules/insights.spec.ts
index fa5c9a72057..6bc7a56b088 100644
--- a/packages/compass-aggregations/src/modules/insights.spec.ts
+++ b/packages/compass-aggregations/src/modules/insights.spec.ts
@@ -44,14 +44,16 @@ describe('fetchExplainForPipeline', function () {
explainAggregate: Sinon.stub().resolves(simpleExplain),
};
- const store = configureStore(
- {
- namespace: 'test.test',
- },
- dataService
- );
+ const store = (
+ await configureStore(
+ {
+ namespace: 'test.test',
+ },
+ dataService
+ )
+ ).plugin.store;
- await store.dispatch(fetchExplainForPipeline() as any);
+ await store.dispatch(fetchExplainForPipeline());
expect(store.getState()).to.have.nested.property(
'insights.isCollectionScan',
@@ -64,14 +66,16 @@ describe('fetchExplainForPipeline', function () {
explainAggregate: Sinon.stub().resolves(explainWithIndex),
};
- const store = configureStore(
- {
- namespace: 'test.test',
- },
- dataService
- );
+ const store = (
+ await configureStore(
+ {
+ namespace: 'test.test',
+ },
+ dataService
+ )
+ ).plugin.store;
- await store.dispatch(fetchExplainForPipeline() as any);
+ await store.dispatch(fetchExplainForPipeline());
expect(store.getState()).to.have.nested.property(
'insights.isCollectionScan',
@@ -84,18 +88,21 @@ describe('fetchExplainForPipeline', function () {
explainAggregate: Sinon.stub().resolves(explainWithIndex),
};
- const store = configureStore(
- {
- namespace: 'test.test',
- },
- dataService
- );
+ const store = (
+ await configureStore(
+ {
+ namespace: 'test.test',
+ },
+ dataService
+ )
+ ).plugin.store;
+
+ void store.dispatch(fetchExplainForPipeline());
+ void store.dispatch(fetchExplainForPipeline());
+ void store.dispatch(fetchExplainForPipeline());
+ void store.dispatch(fetchExplainForPipeline());
- void store.dispatch(fetchExplainForPipeline() as any);
- void store.dispatch(fetchExplainForPipeline() as any);
- void store.dispatch(fetchExplainForPipeline() as any);
- void store.dispatch(fetchExplainForPipeline() as any);
- await store.dispatch(fetchExplainForPipeline() as any);
+ await store.dispatch(fetchExplainForPipeline());
expect(dataService.explainAggregate).to.be.calledOnce;
});
@@ -106,15 +113,17 @@ describe('fetchExplainForPipeline', function () {
isCancelError: Sinon.stub().returns(false),
};
- const store = configureStore(
- {
- namespace: 'test.test',
- pipeline: [{ $match: { foo: 1 } }, { $out: 'test' }],
- },
- dataService
- );
+ const store = (
+ await configureStore(
+ {
+ namespace: 'test.test',
+ pipeline: [{ $match: { foo: 1 } }, { $out: 'test' }],
+ },
+ dataService
+ )
+ ).plugin.store;
- await store.dispatch(fetchExplainForPipeline() as any);
+ await store.dispatch(fetchExplainForPipeline());
expect(dataService.explainAggregate).to.be.calledWith('test.test', [
{ $match: { foo: 1 } },
@@ -127,15 +136,17 @@ describe('fetchExplainForPipeline', function () {
isCancelError: Sinon.stub().returns(false),
};
- const store = configureStore(
- {
- namespace: 'test.test',
- pipeline: [{ $merge: { into: 'test' } }, { $match: { bar: 2 } }],
- },
- dataService
- );
+ const store = (
+ await configureStore(
+ {
+ namespace: 'test.test',
+ pipeline: [{ $merge: { into: 'test' } }, { $match: { bar: 2 } }],
+ },
+ dataService
+ )
+ ).plugin.store;
- await store.dispatch(fetchExplainForPipeline() as any);
+ await store.dispatch(fetchExplainForPipeline());
expect(dataService.explainAggregate).to.be.calledWith('test.test', [
{ $match: { bar: 2 } },
diff --git a/packages/compass-aggregations/src/modules/max-time-ms.spec.ts b/packages/compass-aggregations/src/modules/max-time-ms.spec.ts
index af48d5ef520..41892f98e7a 100644
--- a/packages/compass-aggregations/src/modules/max-time-ms.spec.ts
+++ b/packages/compass-aggregations/src/modules/max-time-ms.spec.ts
@@ -1,15 +1,18 @@
import { maxTimeMSChanged } from './max-time-ms';
import { expect } from 'chai';
import configureStore from '../../test/configure-store';
+import type { AggregationsStore } from '../stores/store';
describe('max-time-ms module', function () {
- let store: ReturnType;
- beforeEach(function () {
- store = configureStore(undefined, undefined, {
- preferences: {
- getPreferences: () => ({ maxTimeMS: 1000 }),
- },
- } as any);
+ let store: AggregationsStore;
+ beforeEach(async function () {
+ store = (
+ await configureStore(undefined, undefined, {
+ preferences: {
+ getPreferences: () => ({ maxTimeMS: 1000 }),
+ } as any,
+ })
+ ).plugin.store;
});
it('initializes default max time to preferences value', function () {
diff --git a/packages/compass-aggregations/src/modules/pipeline-builder/builder-helpers.spec.ts b/packages/compass-aggregations/src/modules/pipeline-builder/builder-helpers.spec.ts
index c24a5be9dae..6d345e0d4e3 100644
--- a/packages/compass-aggregations/src/modules/pipeline-builder/builder-helpers.spec.ts
+++ b/packages/compass-aggregations/src/modules/pipeline-builder/builder-helpers.spec.ts
@@ -3,16 +3,20 @@ import { getPipelineStageOperatorsFromBuilderState } from './builder-helpers';
import { addStage } from './stage-editor';
import { changePipelineMode } from './pipeline-mode';
import configureStore from '../../../test/configure-store';
+import type { AggregationsStore } from '../../stores/store';
-function createStore(pipelineText = `[{$match: {_id: 1}}, {$limit: 10}]`) {
- return configureStore({ pipelineText });
+async function createStore(
+ pipelineText = `[{$match: {_id: 1}}, {$limit: 10}]`
+) {
+ const result = await configureStore({ pipelineText });
+ return result.plugin.store;
}
describe('builder-helpers', function () {
describe('getPipelineStageOperatorsFromBuilderState', function () {
- let store: ReturnType;
- beforeEach(function () {
- store = createStore();
+ let store: AggregationsStore;
+ beforeEach(async function () {
+ store = await createStore();
});
describe('in stage editor mode', function () {
it('should return filtered stage names', function () {
diff --git a/packages/compass-aggregations/src/modules/pipeline-builder/pipeline-ai.spec.ts b/packages/compass-aggregations/src/modules/pipeline-builder/pipeline-ai.spec.ts
index 372aecca22b..1d2fefdc0a2 100644
--- a/packages/compass-aggregations/src/modules/pipeline-builder/pipeline-ai.spec.ts
+++ b/packages/compass-aggregations/src/modules/pipeline-builder/pipeline-ai.spec.ts
@@ -4,8 +4,9 @@ import type { PreferencesAccess } from 'compass-preferences-model';
import { createSandboxFromDefaultPreferences } from 'compass-preferences-model';
import type { AtlasAiService } from '@mongodb-js/compass-generative-ai/provider';
import type { DataService } from 'mongodb-data-service';
-
-import configureReduxStore from '../../../test/configure-store';
+import configureReduxStore, {
+ MockAtlasAiService,
+} from '../../../test/configure-store';
import {
AIPipelineActionTypes,
cancelAIPipelineGeneration,
@@ -13,8 +14,6 @@ import {
generateAggregationFromQuery,
} from './pipeline-ai';
import { toggleAutoPreview } from '../auto-preview';
-import { MockAtlasAiService } from '../../../test/configure-store';
-import { createNoopTrack } from '@mongodb-js/compass-telemetry/provider';
describe('AIPipelineReducer', function () {
const sandbox = Sinon.createSandbox();
@@ -34,26 +33,26 @@ describe('AIPipelineReducer', function () {
sandbox.reset();
});
- function configureStore(
+ async function configureStore(
aiService: Partial = {},
mockDataService?: Partial
) {
const atlasAiService = Object.assign(new MockAtlasAiService(), aiService);
- return configureReduxStore(
+ const result = await configureReduxStore(
{
namespace: 'database.collection',
},
{
sample: sandbox.stub().resolves([{ _id: 42 }]),
getConnectionString: sandbox.stub().returns({ hosts: [] }),
+ ...mockDataService,
} as any,
{
atlasAiService: atlasAiService as any,
- dataService: mockDataService as any,
preferences,
- track: createNoopTrack(),
}
);
+ return result.plugin.store;
}
describe('runAIPipelineGeneration', function () {
@@ -62,7 +61,7 @@ describe('AIPipelineReducer', function () {
const fetchJsonStub = sandbox.stub().resolves({
content: { aggregation: { pipeline: '[{ $match: { _id: 1 } }]' } },
});
- const store = configureStore({
+ const store = await configureStore({
getAggregationFromUserInput: fetchJsonStub,
});
@@ -98,7 +97,7 @@ describe('AIPipelineReducer', function () {
describe('when there is an error', function () {
it('sets the error on the store', async function () {
- const store = configureStore({
+ const store = await configureStore({
getAggregationFromUserInput: sandbox
.stub()
.rejects(new Error('500 Internal Server Error')),
@@ -121,7 +120,7 @@ describe('AIPipelineReducer', function () {
it('resets the store if errs was caused by user being unauthorized', async function () {
const authError = new Error('Unauthorized');
(authError as any).statusCode = 401;
- const store = configureStore({
+ const store = await configureStore({
getAggregationFromUserInput: sandbox.stub().rejects(authError),
});
await store.dispatch(runAIPipelineGeneration('testing prompt') as any);
@@ -152,7 +151,7 @@ describe('AIPipelineReducer', function () {
const mockDataService = {
sample: sandbox.stub().resolves([{ pineapple: 'turtle' }]),
};
- const store = configureStore(
+ const store = await configureStore(
{
getAggregationFromUserInput: fetchJsonStub,
},
@@ -185,7 +184,7 @@ describe('AIPipelineReducer', function () {
const fetchJsonStub = sandbox.stub().resolves({
content: { aggregation: { pipeline: '[{ $match: { _id: 1 } }]' } },
});
- const store = configureStore({
+ const store = await configureStore({
getAggregationFromUserInput: fetchJsonStub,
});
@@ -206,8 +205,8 @@ describe('AIPipelineReducer', function () {
});
describe('cancelAIPipelineGeneration', function () {
- it('should unset the fetching id and set the status on the store', function () {
- const store = configureStore();
+ it('should unset the fetching id and set the status on the store', async function () {
+ const store = await configureStore();
expect(
store.getState().pipelineBuilder.aiPipeline.aiPipelineRequestId
).to.equal(null);
@@ -236,8 +235,8 @@ describe('AIPipelineReducer', function () {
});
describe('generateAggregationFromQuery', function () {
- it('should create an aggregation pipeline', function () {
- const store = configureStore({
+ it('should create an aggregation pipeline', async function () {
+ const store = await configureStore({
getAggregationFromUserInput: sandbox.stub().resolves({
content: {
aggregation: { pipeline: '[{ $group: { _id: "$price" } }]' },
diff --git a/packages/compass-aggregations/src/modules/search-indexes.spec.ts b/packages/compass-aggregations/src/modules/search-indexes.spec.ts
index ab3dd5ac800..c9be316c4ca 100644
--- a/packages/compass-aggregations/src/modules/search-indexes.spec.ts
+++ b/packages/compass-aggregations/src/modules/search-indexes.spec.ts
@@ -3,6 +3,7 @@ import reducer, { fetchIndexes, ActionTypes } from './search-indexes';
import configureStore from '../../test/configure-store';
import sinon from 'sinon';
import type { AnyAction } from 'redux';
+import type { AggregationsStore } from '../stores/store';
describe('search-indexes module', function () {
describe('#reducer', function () {
@@ -55,20 +56,22 @@ describe('search-indexes module', function () {
describe('#actions', function () {
let getSearchIndexesStub: sinon.SinonStub;
let sandbox: sinon.SinonSandbox;
- let store: ReturnType;
- beforeEach(function () {
+ let store: AggregationsStore;
+ beforeEach(async function () {
sandbox = sinon.createSandbox();
getSearchIndexesStub = sandbox.stub();
- store = configureStore(
- {
- pipeline: [],
- isSearchIndexesSupported: true,
- namespace: 'test.listings',
- },
- {
- getSearchIndexes: getSearchIndexesStub,
- } as any
- );
+ store = (
+ await configureStore(
+ {
+ pipeline: [],
+ isSearchIndexesSupported: true,
+ namespace: 'test.listings',
+ },
+ {
+ getSearchIndexes: getSearchIndexesStub,
+ } as any
+ )
+ ).plugin.store;
});
context('fetchIndexes', function () {
it('fetches search indexes and sets status to READY', async function () {
diff --git a/packages/compass-aggregations/src/modules/side-panel.spec.ts b/packages/compass-aggregations/src/modules/side-panel.spec.ts
index f41e336577e..f149587e215 100644
--- a/packages/compass-aggregations/src/modules/side-panel.spec.ts
+++ b/packages/compass-aggregations/src/modules/side-panel.spec.ts
@@ -11,8 +11,8 @@ describe('side-panel module', function () {
let store: Store;
let fakeLocalStorage: SinonStub;
- beforeEach(function () {
- store = configureStore();
+ beforeEach(async function () {
+ store = (await configureStore()).plugin.store;
const localStorageValues: Record = {};
@@ -41,14 +41,14 @@ describe('side-panel module', function () {
expect(store.getState().sidePanel.isPanelOpen).to.equal(false);
});
- it('persists the last state', function () {
- const store1 = configureStore();
+ it('persists the last state', async function () {
+ const store1 = (await configureStore()).plugin.store;
expect(store1.getState().sidePanel.isPanelOpen).to.equal(false);
store1.dispatch(toggleSidePanel() as any);
expect(store1.getState().sidePanel.isPanelOpen).to.equal(true);
- const store2 = configureStore();
+ const store2 = (await configureStore()).plugin.store;
expect(store2.getState().sidePanel.isPanelOpen).to.equal(true);
});
});
diff --git a/packages/compass-aggregations/src/plugin.spec.tsx b/packages/compass-aggregations/src/plugin.spec.tsx
index a65701d6eac..89970482e78 100644
--- a/packages/compass-aggregations/src/plugin.spec.tsx
+++ b/packages/compass-aggregations/src/plugin.spec.tsx
@@ -1,23 +1,15 @@
import React from 'react';
-import { render, screen } from '@testing-library/react';
+import { screen } from '@testing-library/react';
import { expect } from 'chai';
-import configureStore from '../test/configure-store';
+import { renderWithStore } from '../test/configure-store';
import { AggregationsPlugin } from './plugin';
-import { Provider } from 'react-redux';
-
-const renderPlugin = () => {
- const store = configureStore();
- const metadata = {} as any;
- render(
-
-
-
- );
-};
describe('Aggregations [Plugin]', function () {
- it('should render plugin with toolbar and export button', function () {
- renderPlugin();
+ it('should render plugin with toolbar and export button', async function () {
+ const metadata = {} as any;
+ await renderWithStore(
+
+ );
expect(screen.getByTestId('pipeline-toolbar')).to.exist;
expect(screen.getByTestId('pipeline-toolbar-export-aggregation-button')).to
.exist;
diff --git a/packages/compass-aggregations/src/stores/create-view.spec.ts b/packages/compass-aggregations/src/stores/create-view.spec.ts
index 51957e95750..35e5f562768 100644
--- a/packages/compass-aggregations/src/stores/create-view.spec.ts
+++ b/packages/compass-aggregations/src/stores/create-view.spec.ts
@@ -1,14 +1,19 @@
-import AppRegistry, { createActivateHelpers } from 'hadron-app-registry';
-import { activateCreateViewPlugin } from './create-view';
+import type AppRegistry from 'hadron-app-registry';
import { expect } from 'chai';
-import {
- ConnectionsManager,
- type DataService,
- type ConnectionRepository,
-} from '@mongodb-js/compass-connections/provider';
import { changeViewName, createView } from '../modules/create-view';
import Sinon from 'sinon';
-import type { WorkspacesService } from '@mongodb-js/compass-workspaces/provider';
+import {
+ activatePluginWithConnections,
+ cleanup,
+} from '@mongodb-js/compass-connections/test';
+import { CreateViewPlugin } from '../index';
+
+const TEST_CONNECTION = {
+ id: 'TEST',
+ connectionOptions: {
+ connectionString: 'mongodb://localhost:27017',
+ },
+};
describe('CreateViewStore [Store]', function () {
if (
@@ -21,69 +26,60 @@ describe('CreateViewStore [Store]', function () {
}
let store: any;
- let deactivate: any;
- let globalAppRegistry: AppRegistry;
- let appRegistryEmitSpy: Sinon.SinonSpy;
- const logger = {} as any;
- const track = () => {};
- const createViewStub = Sinon.stub();
- const dataService = {
- createView: createViewStub,
- } as unknown as DataService;
- const connectionsManager = new ConnectionsManager({ logger });
- const openCollectionWorkspaceStub = Sinon.stub();
+ let appRegistry: Sinon.SinonSpiedInstance;
+
const workspaces = {
- openCollectionWorkspace: openCollectionWorkspaceStub,
- } as unknown as WorkspacesService;
- const connectionRepository = {
- getConnectionInfoById: () => {},
- } as unknown as ConnectionRepository;
+ openCollectionWorkspace: Sinon.stub(),
+ } as any;
+
+ const dataService = {
+ createView: Sinon.stub(),
+ };
+
+ beforeEach(async function () {
+ const { plugin, globalAppRegistry, connectionsStore } =
+ activatePluginWithConnections(
+ CreateViewPlugin.withMockServices({ workspaces }) as any,
+ {},
+ {
+ connections: [TEST_CONNECTION],
+ connectFn() {
+ return dataService;
+ },
+ }
+ );
+
+ await connectionsStore.actions.connect(TEST_CONNECTION);
- beforeEach(function () {
- globalAppRegistry = new AppRegistry();
- appRegistryEmitSpy = Sinon.spy(globalAppRegistry, 'emit');
- Sinon.stub(connectionsManager, 'getDataServiceForConnection').returns(
- dataService
- );
- ({ store, deactivate } = activateCreateViewPlugin(
- {},
- {
- globalAppRegistry,
- connectionsManager,
- connectionRepository,
- logger,
- track,
- workspaces,
- },
- createActivateHelpers()
- ));
+ store = plugin.store;
+ appRegistry = Sinon.spy(globalAppRegistry);
});
afterEach(function () {
store = null;
Sinon.restore();
- deactivate();
+ cleanup();
});
describe('#configureStore', function () {
describe('when open create view is emitted', function () {
it('throws an error when the action is emitted without connection meta', function () {
expect(() => {
- globalAppRegistry.emit('open-create-view', {
+ appRegistry.emit('open-create-view', {
source: 'dataService.test',
pipeline: [{ $project: { a: 1 } }],
});
}).to.throw;
});
it('dispatches the open action and sets the correct state', function () {
- globalAppRegistry.emit(
+ appRegistry.emit(
'open-create-view',
{
source: 'dataService.test',
pipeline: [{ $project: { a: 1 } }],
},
{
- connectionId: 'TEST',
+ connectionId: TEST_CONNECTION.id,
}
);
expect(store.getState().isVisible).to.equal(true);
@@ -91,39 +87,39 @@ describe('CreateViewStore [Store]', function () {
{ $project: { a: 1 } },
]);
expect(store.getState().source).to.equal('dataService.test');
- expect(store.getState().connectionId).to.equal('TEST');
+ expect(store.getState().connectionId).to.equal(TEST_CONNECTION.id);
});
});
it('handles createView action and notifies the rest of the app', async function () {
- globalAppRegistry.emit(
+ appRegistry.emit(
'open-create-view',
{
source: 'dataService.test',
pipeline: [{ $project: { a: 1 } }],
},
{
- connectionId: 'TEST',
+ connectionId: TEST_CONNECTION.id,
}
);
store.dispatch(changeViewName('TestView'));
await store.dispatch(createView());
- expect(createViewStub).to.be.calledWithExactly(
+ expect(dataService.createView).to.be.calledWithExactly(
'TestView',
'dataService.test',
[{ $project: { a: 1 } }],
{}
);
- expect(appRegistryEmitSpy.lastCall).to.be.calledWithExactly(
+ expect(appRegistry.emit.lastCall).to.be.calledWithExactly(
'view-created',
'dataService.TestView',
- { connectionId: 'TEST' }
+ { connectionId: TEST_CONNECTION.id }
);
- expect(openCollectionWorkspaceStub).to.be.calledWithExactly(
- 'TEST',
+ expect(workspaces.openCollectionWorkspace).to.be.calledWithExactly(
+ TEST_CONNECTION.id,
'dataService.TestView',
{ newTab: true }
);
diff --git a/packages/compass-aggregations/src/stores/store.spec.ts b/packages/compass-aggregations/src/stores/store.spec.ts
index dd74ad895e1..b2724ab3d9b 100644
--- a/packages/compass-aggregations/src/stores/store.spec.ts
+++ b/packages/compass-aggregations/src/stores/store.spec.ts
@@ -1,20 +1,21 @@
-import AppRegistry from 'hadron-app-registry';
+import type AppRegistry from 'hadron-app-registry';
import rootReducer from '../modules';
import { expect } from 'chai';
import configureStore from '../../test/configure-store';
-import type { Store } from 'redux';
+import type { AggregationsStore } from '../stores/store';
const INITIAL_STATE = rootReducer(undefined, { type: '@@init' });
describe('Aggregation Store', function () {
describe('#configureStore', function () {
context('when providing a serverVersion', function () {
- let store: Store;
+ let store: AggregationsStore;
- beforeEach(function () {
- store = configureStore({
+ beforeEach(async function () {
+ const result = await configureStore({
serverVersion: '4.2.0',
});
+ store = result.plugin.store;
});
it('sets the server version the state', function () {
@@ -23,12 +24,13 @@ describe('Aggregation Store', function () {
});
context('when providing an env', function () {
- let store: Store;
+ let store: AggregationsStore;
- beforeEach(function () {
- store = configureStore({
+ beforeEach(async function () {
+ const result = await configureStore({
env: 'atlas',
});
+ store = result.plugin.store;
});
it('sets the env in the state', function () {
@@ -37,17 +39,12 @@ describe('Aggregation Store', function () {
});
context('when providing a namespace', function () {
- context('when there is no collection', function () {
- it('throws', function () {
- expect(() => configureStore({ namespace: 'db' })).to.throw();
- });
- });
-
context('when there is a collection', function () {
- let store: Store;
+ let store: AggregationsStore;
- beforeEach(function () {
- store = configureStore({ namespace: 'db.coll' });
+ beforeEach(async function () {
+ const result = await configureStore({ namespace: 'db.coll' });
+ store = result.plugin.store;
});
it('updates the namespace in the store', function () {
@@ -55,15 +52,15 @@ describe('Aggregation Store', function () {
});
it('resets the rest of the state to initial state', function () {
+ // Remove properties that we don't want to compare
// eslint-disable-next-line @typescript-eslint/no-unused-vars
- const { aggregationWorkspaceId, dataService, ...state } =
+ const { aggregationWorkspaceId, dataService, sidePanel, ...state } =
store.getState();
+
state.pipelineBuilder.stageEditor = {
stages: [],
stagesIdAndType: [],
};
- delete state.pipeline;
- delete state.sidePanel;
expect(state).to.deep.equal({
outResultsFn: INITIAL_STATE.outResultsFn,
@@ -79,7 +76,6 @@ describe('Aggregation Store', function () {
savedPipeline: INITIAL_STATE.savedPipeline,
inputDocuments: {
...INITIAL_STATE.inputDocuments,
- isLoading: true,
},
serverVersion: INITIAL_STATE.serverVersion,
isModified: INITIAL_STATE.isModified,
@@ -109,15 +105,13 @@ describe('Aggregation Store', function () {
});
describe('#onActivated', function () {
- let store: Store;
- const localAppRegistry = new AppRegistry();
- const globalAppRegistry = new AppRegistry();
-
- beforeEach(function () {
- store = configureStore(undefined, undefined, {
- localAppRegistry: localAppRegistry,
- globalAppRegistry: globalAppRegistry,
- });
+ let store: AggregationsStore;
+ let localAppRegistry: AppRegistry;
+
+ beforeEach(async function () {
+ const result = await configureStore();
+ localAppRegistry = result.localAppRegistry;
+ store = result.plugin.store;
});
context('when an aggregation should be generated from query', function () {
diff --git a/packages/compass-aggregations/src/stores/store.ts b/packages/compass-aggregations/src/stores/store.ts
index 410f0330348..993a6f50dd0 100644
--- a/packages/compass-aggregations/src/stores/store.ts
+++ b/packages/compass-aggregations/src/stores/store.ts
@@ -316,3 +316,7 @@ const handleDatabaseCollections = (
onDatabaseCollectionStatusChange
);
};
+
+export type AggregationsStore = ReturnType<
+ typeof activateAggregationsPlugin
+>['store'];
diff --git a/packages/compass-aggregations/test/configure-store.ts b/packages/compass-aggregations/test/configure-store.ts
index 18c7c1ddbf4..9c2fe89b090 100644
--- a/packages/compass-aggregations/test/configure-store.ts
+++ b/packages/compass-aggregations/test/configure-store.ts
@@ -1,19 +1,17 @@
-import AppRegistry, { createActivateHelpers } from 'hadron-app-registry';
import type {
AggregationsPluginServices,
ConfigureStoreOptions,
} from '../src/stores/store';
-import { activateAggregationsPlugin } from '../src/stores/store';
import { mockDataService } from './mocks/data-service';
-import type { DataService } from '../src/modules/data-service';
-import { ReadOnlyPreferenceAccess } from 'compass-preferences-model/provider';
-import { createNoopLogger } from '@mongodb-js/compass-logging/provider';
-import { createNoopTrack } from '@mongodb-js/compass-telemetry/provider';
import { AtlasAuthService } from '@mongodb-js/atlas-service/provider';
import {
- ConnectionScopedAppRegistryImpl,
- TEST_CONNECTION_INFO,
-} from '@mongodb-js/compass-connections/provider';
+ activatePluginWithActiveConnection,
+ renderPluginComponentWithActiveConnection,
+} from '@mongodb-js/compass-connections/test';
+import { CompassAggregationsHadronPlugin } from '../src/index';
+import type { DataService } from '@mongodb-js/compass-connections/provider';
+import React from 'react';
+import { PipelineStorageProvider } from '@mongodb-js/my-queries-storage/provider';
export class MockAtlasAuthService extends AtlasAuthService {
isAuthenticated() {
@@ -42,30 +40,24 @@ export class MockAtlasAiService {
}
}
-export default function configureStore(
- options: Partial = {},
- dataService: DataService = mockDataService(),
+function getMockedPluginArgs(
+ initialProps: Partial = {},
+ dataService: Partial = mockDataService(),
services: Partial = {}
) {
- const preferences = new ReadOnlyPreferenceAccess();
- const logger = createNoopLogger();
- const track = createNoopTrack();
-
const atlasAuthService = new MockAtlasAuthService();
const atlasAiService = new MockAtlasAiService();
- const globalAppRegistry = new AppRegistry();
- const connectionInfoAccess = {
- getCurrentConnectionInfo() {
- return TEST_CONNECTION_INFO;
- },
- };
- const connectionScopedAppRegistry =
- new ConnectionScopedAppRegistryImpl<'open-export'>(
- globalAppRegistry.emit.bind(globalAppRegistry),
- connectionInfoAccess
- );
-
- return activateAggregationsPlugin(
+ return [
+ CompassAggregationsHadronPlugin.withMockServices({
+ atlasAuthService,
+ atlasAiService,
+ collection: {
+ toJSON: () => ({}),
+ on: () => {},
+ removeListener: () => {},
+ } as any,
+ ...services,
+ } as any),
{
namespace: 'test.test',
isReadonly: false,
@@ -76,27 +68,47 @@ export default function configureStore(
isDataLake: false,
isAtlas: false,
serverVersion: '4.0.0',
- ...options,
+ ...initialProps,
},
{
- dataService,
- instance: {} as any,
- preferences,
- globalAppRegistry,
- localAppRegistry: new AppRegistry(),
- workspaces: {} as any,
- logger,
- track,
- atlasAiService: atlasAiService as any,
- atlasAuthService,
- connectionInfoAccess,
- collection: {
- toJSON: () => ({}),
- on: () => {},
- } as any,
- connectionScopedAppRegistry,
- ...services,
+ id: 'TEST',
+ connectionOptions: {
+ connectionString: 'mongodb://localhost:27020',
+ },
},
- createActivateHelpers()
- ).store;
+ {
+ connectFn() {
+ return dataService;
+ },
+ preferences: services.preferences
+ ? services.preferences.getPreferences()
+ : undefined,
+ },
+ ] as unknown as Parameters;
+}
+
+/**
+ * @deprecated use renderWithStore and test store through UI instead
+ */
+export default function configureStore(
+ ...args: Parameters
+) {
+ return activatePluginWithActiveConnection(...getMockedPluginArgs(...args));
+}
+
+export function renderWithStore(
+ ui: React.ReactElement,
+ ...args: Parameters
+) {
+ ui = args[2]?.pipelineStorage
+ ? React.createElement(PipelineStorageProvider, {
+ value: args[2].pipelineStorage,
+ children: ui,
+ })
+ : ui;
+
+ return renderPluginComponentWithActiveConnection(
+ ui,
+ ...getMockedPluginArgs(...args)
+ );
}
diff --git a/packages/compass-app-stores/package.json b/packages/compass-app-stores/package.json
index 2b5ea66daa0..1823a458517 100644
--- a/packages/compass-app-stores/package.json
+++ b/packages/compass-app-stores/package.json
@@ -11,7 +11,7 @@
"email": "compass@mongodb.com"
},
"homepage": "https://github.com/mongodb-js/compass",
- "version": "7.24.0",
+ "version": "7.25.0",
"repository": {
"type": "git",
"url": "https://github.com/mongodb-js/compass.git"
@@ -53,12 +53,10 @@
"reformat": "npm run eslint . -- --fix && npm run prettier -- --write ."
},
"devDependencies": {
- "@mongodb-js/eslint-config-compass": "^1.1.4",
- "@mongodb-js/mocha-config-compass": "^1.3.10",
+ "@mongodb-js/eslint-config-compass": "^1.1.5",
+ "@mongodb-js/mocha-config-compass": "^1.4.0",
"@mongodb-js/prettier-config-compass": "^1.0.2",
"@mongodb-js/tsconfig-compass": "^1.0.4",
- "@testing-library/dom": "^8.20.1",
- "@testing-library/react": "^12.1.5",
"@types/chai": "^4.2.21",
"@types/mocha": "^9.0.0",
"@types/sinon-chai": "^3.2.5",
@@ -74,14 +72,14 @@
"xvfb-maybe": "^0.2.1"
},
"dependencies": {
- "@mongodb-js/compass-components": "^1.29.0",
- "@mongodb-js/compass-connections": "^1.38.0",
- "@mongodb-js/compass-logging": "^1.4.3",
- "@mongodb-js/connection-info": "^0.5.3",
- "hadron-app-registry": "^9.2.2",
- "mongodb-collection-model": "^5.22.3",
- "mongodb-database-model": "^2.22.3",
- "mongodb-instance-model": "^12.23.3",
+ "@mongodb-js/compass-components": "^1.29.1",
+ "@mongodb-js/compass-connections": "^1.39.0",
+ "@mongodb-js/compass-logging": "^1.4.4",
+ "@mongodb-js/connection-info": "^0.6.0",
+ "hadron-app-registry": "^9.2.3",
+ "mongodb-collection-model": "^5.23.0",
+ "mongodb-database-model": "^2.23.0",
+ "mongodb-instance-model": "^12.24.0",
"mongodb-ns": "^2.4.2",
"react": "^17.0.2"
},
diff --git a/packages/compass-app-stores/src/plugin.tsx b/packages/compass-app-stores/src/plugin.tsx
index 86f682c7895..8903e22be57 100644
--- a/packages/compass-app-stores/src/plugin.tsx
+++ b/packages/compass-app-stores/src/plugin.tsx
@@ -13,7 +13,7 @@ import {
import { type MongoDBInstancesManager } from './instances-manager';
interface MongoDBInstancesProviderProps {
- children: React.ReactNode;
+ children?: React.ReactNode;
instancesManager: MongoDBInstancesManager;
}
@@ -28,13 +28,7 @@ function MongoDBInstancesManagerProvider({
);
}
-export const CompassInstanceStorePlugin = registerHadronPlugin<
- { children: React.ReactNode },
- {
- logger: () => Logger;
- connectionsManager: () => ConnectionsManager;
- }
->(
+export const CompassInstanceStorePlugin = registerHadronPlugin(
{
name: 'CompassInstanceStore',
component: MongoDBInstancesManagerProvider as React.FunctionComponent<
diff --git a/packages/compass-app-stores/src/provider.spec.tsx b/packages/compass-app-stores/src/provider.spec.tsx
index 15070433863..65e21819ac9 100644
--- a/packages/compass-app-stores/src/provider.spec.tsx
+++ b/packages/compass-app-stores/src/provider.spec.tsx
@@ -4,10 +4,14 @@ import {
MongoDBInstancesManagerProvider,
TestMongoDBInstanceManager,
} from './provider';
-import { render, screen, cleanup } from '@testing-library/react';
import { expect } from 'chai';
-import { waitFor } from '@testing-library/dom';
import Sinon from 'sinon';
+import {
+ renderWithActiveConnection,
+ screen,
+ cleanup,
+ waitFor,
+} from '@mongodb-js/compass-connections/test';
describe('NamespaceProvider', function () {
const sandbox = Sinon.createSandbox();
@@ -17,11 +21,11 @@ describe('NamespaceProvider', function () {
sandbox.reset();
});
- it('should immediately render content if database exists', function () {
+ it('should immediately render content if database exists', async function () {
const instanceManager = new TestMongoDBInstanceManager({
databases: [{ _id: 'foo' }] as any,
});
- render(
+ await renderWithActiveConnection(
hello
@@ -29,11 +33,11 @@ describe('NamespaceProvider', function () {
expect(screen.getByText('hello')).to.exist;
});
- it('should immediately render content if collection exists', function () {
+ it('should immediately render content if collection exists', async function () {
const instanceManager = new TestMongoDBInstanceManager({
databases: [{ _id: 'foo', collections: [{ _id: 'foo.bar' }] }] as any,
});
- render(
+ await renderWithActiveConnection(
hello
@@ -41,9 +45,9 @@ describe('NamespaceProvider', function () {
expect(screen.getByText('hello')).to.exist;
});
- it("should not render content when namespace doesn't exist", function () {
+ it("should not render content when namespace doesn't exist", async function () {
const instanceManager = new TestMongoDBInstanceManager();
- render(
+ await renderWithActiveConnection(
hello
@@ -59,7 +63,7 @@ describe('NamespaceProvider', function () {
return Promise.resolve();
});
- render(
+ await renderWithActiveConnection(
hello
@@ -77,7 +81,7 @@ describe('NamespaceProvider', function () {
const instanceManager = new TestMongoDBInstanceManager({
databases: [{ _id: 'foo' }] as any,
});
- render(
+ await renderWithActiveConnection(
;
let instancesManager: MongoDBInstancesManager;
-
let sandbox: sinon.SinonSandbox;
+ let getDataService: any;
+ let connectionsStore: any;
function waitForInstanceRefresh(instance: MongoDBInstance): Promise {
return new Promise((resolve) => {
@@ -70,65 +54,47 @@ describe('InstanceStore [Store]', function () {
}
beforeEach(function () {
- globalAppRegistry = new AppRegistry();
- sandbox = sinon.createSandbox();
-
- dataService = createDataService();
- const logger = createNoopLogger();
- connectionsManager = new ConnectionsManager({
- logger: logger.log.unbound,
- __TEST_CONNECT_FN: () => Promise.resolve(dataService),
- });
-
- store = createInstancesStore(
+ const result = activatePluginWithConnections(
+ CompassInstanceStorePlugin,
+ {},
{
- connectionsManager,
- globalAppRegistry,
- logger,
- },
- createActivateHelpers()
+ connectFn() {
+ return createDataService();
+ },
+ }
);
- instancesManager = store.getState().instancesManager;
+ connectionsStore = result.connectionsStore;
+ getDataService = result.getDataServiceForConnection;
+ globalAppRegistry = result.globalAppRegistry;
+ sandbox = sinon.createSandbox();
+ instancesManager = result.plugin.store.getState().instancesManager;
});
afterEach(function () {
sandbox.restore();
- store.deactivate();
+ cleanup();
});
it('should not have any MongoDBInstance if no connection is established', function () {
expect(instancesManager.listMongoDBInstances()).to.be.of.length(0);
});
- it('should have a MongodbInstance for each of the connected connection', function () {
- for (const connectedConnectionInfoId of ['1', '2', '3']) {
- connectionsManager.emit(
- ConnectionsManagerEvents.ConnectionAttemptSuccessful,
- connectedConnectionInfoId,
- dataService
- );
+ it('should have a MongodbInstance for each of the connected connection', async function () {
+ for (const connectionInfo of mockConnections) {
+ await connectionsStore.actions.connect(connectionInfo);
+ expect(() => {
+ instancesManager.getMongoDBInstanceForConnection(connectionInfo.id);
+ }).to.not.throw();
}
-
- expect(() => instancesManager.getMongoDBInstanceForConnection('1')).to.not
- .throw;
- expect(() => instancesManager.getMongoDBInstanceForConnection('2')).to.not
- .throw;
- expect(() => instancesManager.getMongoDBInstanceForConnection('3')).to.not
- .throw;
});
context('when connected', function () {
let connectedInstance: MongoDBInstance;
let initialInstanceRefreshedPromise: Promise;
- beforeEach(function () {
- sinon
- .stub(connectionsManager, 'getDataServiceForConnection')
- .returns(dataService);
- connectionsManager.emit(
- ConnectionsManagerEvents.ConnectionAttemptSuccessful,
- connectedConnectionInfoId,
- dataService
- );
+ const connectedConnectionInfoId = mockConnections[0].id;
+
+ beforeEach(async function () {
+ await connectionsStore.actions.connect(mockConnections[0]);
const instance = instancesManager.getMongoDBInstanceForConnection(
connectedConnectionInfoId
);
@@ -140,20 +106,22 @@ describe('InstanceStore [Store]', function () {
context('on refresh data', function () {
beforeEach(async function () {
- sandbox
- .stub(dataService, 'instance')
- .returns({ build: { version: '3.2.1' } });
await initialInstanceRefreshedPromise;
+ sandbox
+ .stub(getDataService(connectedConnectionInfoId), 'instance')
+ .resolves({ build: { version: '3.2.1' } });
const instance = instancesManager.getMongoDBInstanceForConnection(
connectedConnectionInfoId
);
- expect(instance).to.have.nested.property('build.version', '1.2.3');
+ expect(instance).to.have.nested.property('build.version', '0.0.0');
globalAppRegistry.emit('refresh-data');
await waitForInstanceRefresh(instance);
});
it('calls instance model fetch', function () {
- const instance = instancesManager.getMongoDBInstanceForConnection('1');
+ const instance = instancesManager.getMongoDBInstanceForConnection(
+ connectedConnectionInfoId
+ );
expect(instance).to.have.nested.property('build.version', '3.2.1');
});
});
@@ -163,7 +131,9 @@ describe('InstanceStore [Store]', function () {
await initialInstanceRefreshedPromise;
await Promise.all(
connectedInstance.databases.map((db) => {
- return db.fetchCollections({ dataService });
+ return db.fetchCollections({
+ dataService: getDataService(connectedConnectionInfoId),
+ });
})
);
expect(connectedInstance.databases).to.have.lengthOf(1);
@@ -208,7 +178,7 @@ describe('InstanceStore [Store]', function () {
it('should remove collection from the database collections', function () {
globalAppRegistry.emit('collection-dropped', 'foo.bar', {
- connectionId: '1',
+ connectionId: connectedConnectionInfoId,
});
expect(
connectedInstance.databases.get('foo')?.collections.get('foo.bar')
@@ -222,17 +192,17 @@ describe('InstanceStore [Store]', function () {
coll?.on('change', () => {});
expect((coll as any)._events.change).to.have.lengthOf(1);
globalAppRegistry.emit('collection-dropped', 'foo.bar', {
- connectionId: '1',
+ connectionId: connectedConnectionInfoId,
});
expect((coll as any)._events).to.not.exist;
});
it('should remove database if last collection was removed', function () {
globalAppRegistry.emit('collection-dropped', 'foo.bar', {
- connectionId: '1',
+ connectionId: connectedConnectionInfoId,
});
globalAppRegistry.emit('collection-dropped', 'foo.buz', {
- connectionId: '1',
+ connectionId: connectedConnectionInfoId,
});
expect(connectedInstance.databases).to.have.lengthOf(0);
expect(connectedInstance.databases.get('foo')).not.to.exist;
@@ -261,7 +231,7 @@ describe('InstanceStore [Store]', function () {
it('should remove database from instance databases', function () {
globalAppRegistry.emit('database-dropped', 'foo', {
- connectionId: '1',
+ connectionId: connectedConnectionInfoId,
});
expect(connectedInstance.databases).to.have.lengthOf(0);
expect(connectedInstance.databases.get('foo')).not.to.exist;
@@ -272,7 +242,7 @@ describe('InstanceStore [Store]', function () {
db?.on('change', () => {});
expect((db as any)._events.change).to.have.lengthOf(1);
globalAppRegistry.emit('database-dropped', 'foo', {
- connectionId: '1',
+ connectionId: connectedConnectionInfoId,
});
expect((db as any)._events).to.not.exist;
});
@@ -474,43 +444,33 @@ describe('InstanceStore [Store]', function () {
});
context('when disconnected', function () {
+ const connectionInfo = mockConnections[0];
+ const connectedConnectionInfoId = connectionInfo.id;
+
it('should remove the instance from InstancesManager and should not perform any actions on the stale instance', async function () {
// first connect
- connectionsManager.emit(
- ConnectionsManagerEvents.ConnectionAttemptSuccessful,
- connectedConnectionInfoId,
- dataService
- );
+ await connectionsStore.actions.connect(connectionInfo);
// setup a spy on old instance
const oldInstance = instancesManager.getMongoDBInstanceForConnection(
connectedConnectionInfoId
);
- const oldFetchDatabasesSpy = sinon.spy(oldInstance, 'fetchDatabases');
+ await waitForInstanceRefresh(oldInstance);
- // now disconnect
- connectionsManager.emit(
- ConnectionsManagerEvents.ConnectionDisconnected,
- connectedConnectionInfoId
- );
+ connectionsStore.actions.disconnect(connectedConnectionInfoId);
+
+ // setup a spy on old instance
+ const oldFetchDatabasesSpy = sinon.spy(oldInstance, 'fetchDatabases');
// there is no instance in store InstancesManager now
- expect(() =>
+ expect(() => {
instancesManager.getMongoDBInstanceForConnection(
connectedConnectionInfoId
- )
- ).to.throw;
+ );
+ }).to.throw();
// lets connect again and ensure that old instance does not receive events anymore
- const newDataService = createDataService();
- sinon
- .stub(connectionsManager, 'getDataServiceForConnection')
- .returns(dataService);
- connectionsManager.emit(
- ConnectionsManagerEvents.ConnectionAttemptSuccessful,
- connectedConnectionInfoId,
- newDataService
- );
+ await connectionsStore.actions.connect(connectionInfo);
// setup a spy on new instance
const newInstance = instancesManager.getMongoDBInstanceForConnection(
diff --git a/packages/compass-app-stores/src/stores/instance-store.ts b/packages/compass-app-stores/src/stores/instance-store.ts
index b5a801801c5..379eb2b9090 100644
--- a/packages/compass-app-stores/src/stores/instance-store.ts
+++ b/packages/compass-app-stores/src/stores/instance-store.ts
@@ -5,10 +5,7 @@ import type { DataService } from '@mongodb-js/compass-connections/provider';
import type { ActivateHelpers, AppRegistry } from 'hadron-app-registry';
import type { Logger } from '@mongodb-js/compass-logging/provider';
import { openToast } from '@mongodb-js/compass-components';
-import {
- ConnectionsManagerEvents,
- type ConnectionsManager,
-} from '@mongodb-js/compass-connections/provider';
+import { type ConnectionsManager } from '@mongodb-js/compass-connections/provider';
import { MongoDBInstancesManager } from '../instances-manager';
function serversArray(
@@ -256,79 +253,73 @@ export function createInstancesStore(
}
};
- on(
- connectionsManager,
- ConnectionsManagerEvents.ConnectionDisconnected,
- function (connectionInfoId: string) {
- try {
- const instance =
- instancesManager.getMongoDBInstanceForConnection(connectionInfoId);
- instance.removeAllListeners();
- } catch (error) {
- log.warn(
- mongoLogId(1_001_000_322),
- 'Instance Store',
- 'Failed to remove instance listeners upon disconnect',
- {
- message: (error as Error).message,
- connectionId: connectionInfoId,
- }
- );
- }
- instancesManager.removeMongoDBInstanceForConnection(connectionInfoId);
- }
- );
-
- on(
- connectionsManager,
- ConnectionsManagerEvents.ConnectionAttemptSuccessful,
- function (instanceConnectionId: string, dataService: DataService) {
- const connectionString = dataService.getConnectionString();
- const firstHost = connectionString.hosts[0] || '';
- const [hostname, port] = firstHost.split(':');
-
- const initialInstanceProps: Partial = {
- _id: firstHost,
- hostname: hostname,
- port: port ? +port : undefined,
- topologyDescription: getTopologyDescription(
- dataService.getLastSeenTopology()
- ),
- };
- const instance = instancesManager.createMongoDBInstanceForConnection(
- instanceConnectionId,
- initialInstanceProps as MongoDBInstanceProps
- );
-
- addCleanup(() => {
- instance.removeAllListeners();
- });
-
- void refreshInstance(
- {
- fetchDatabases: true,
- fetchDbStats: true,
- },
+ on(connectionsManager, 'disconnected', function (connectionInfoId: string) {
+ try {
+ const instance =
+ instancesManager.getMongoDBInstanceForConnection(connectionInfoId);
+ instance.removeAllListeners();
+ } catch (error) {
+ log.warn(
+ mongoLogId(1_001_000_322),
+ 'Instance Store',
+ 'Failed to remove instance listeners upon disconnect',
{
- connectionId: instanceConnectionId,
- }
- );
-
- on(
- dataService,
- 'topologyDescriptionChanged',
- ({
- newDescription,
- }: {
- newDescription: ReturnType;
- }) => {
- instance.set({
- topologyDescription: getTopologyDescription(newDescription),
- });
+ message: (error as Error).message,
+ connectionId: connectionInfoId,
}
);
}
- );
+ instancesManager.removeMongoDBInstanceForConnection(connectionInfoId);
+ });
+
+ on(connectionsManager, 'connected', function (instanceConnectionId: string) {
+ const dataService =
+ connectionsManager.getDataServiceForConnection(instanceConnectionId);
+ const connectionString = dataService.getConnectionString();
+ const firstHost = connectionString.hosts[0] || '';
+ const [hostname, port] = firstHost.split(':');
+
+ const initialInstanceProps: Partial = {
+ _id: firstHost,
+ hostname: hostname,
+ port: port ? +port : undefined,
+ topologyDescription: getTopologyDescription(
+ dataService.getLastSeenTopology()
+ ),
+ };
+ const instance = instancesManager.createMongoDBInstanceForConnection(
+ instanceConnectionId,
+ initialInstanceProps as MongoDBInstanceProps
+ );
+
+ addCleanup(() => {
+ instance.removeAllListeners();
+ });
+
+ void refreshInstance(
+ {
+ fetchDatabases: true,
+ fetchDbStats: true,
+ },
+ {
+ connectionId: instanceConnectionId,
+ }
+ );
+
+ on(
+ dataService,
+ 'topologyDescriptionChanged',
+ ({
+ newDescription,
+ }: {
+ newDescription: ReturnType;
+ }) => {
+ instance.set({
+ topologyDescription: getTopologyDescription(newDescription),
+ });
+ }
+ );
+ });
on(
globalAppRegistry,
diff --git a/packages/compass-collection/package.json b/packages/compass-collection/package.json
index a970a2923c7..75ac8d25fb6 100644
--- a/packages/compass-collection/package.json
+++ b/packages/compass-collection/package.json
@@ -11,7 +11,7 @@
"email": "compass@mongodb.com"
},
"homepage": "https://github.com/mongodb-js/compass",
- "version": "4.37.0",
+ "version": "4.38.0",
"repository": {
"type": "git",
"url": "https://github.com/mongodb-js/compass.git"
@@ -48,17 +48,17 @@
"reformat": "npm run eslint . -- --fix && npm run prettier -- --write ."
},
"dependencies": {
- "@mongodb-js/compass-app-stores": "^7.24.0",
- "@mongodb-js/compass-components": "^1.29.0",
- "@mongodb-js/compass-connections": "^1.38.0",
- "@mongodb-js/compass-logging": "^1.4.3",
- "@mongodb-js/compass-telemetry": "^1.1.3",
- "@mongodb-js/compass-workspaces": "^0.19.0",
- "@mongodb-js/connection-info": "^0.5.3",
+ "@mongodb-js/compass-app-stores": "^7.25.0",
+ "@mongodb-js/compass-components": "^1.29.1",
+ "@mongodb-js/compass-connections": "^1.39.0",
+ "@mongodb-js/compass-logging": "^1.4.4",
+ "@mongodb-js/compass-telemetry": "^1.1.4",
+ "@mongodb-js/compass-workspaces": "^0.20.0",
+ "@mongodb-js/connection-info": "^0.6.0",
"@mongodb-js/mongodb-constants": "^0.10.2",
- "compass-preferences-model": "^2.26.0",
- "hadron-app-registry": "^9.2.2",
- "mongodb-collection-model": "^5.22.3",
+ "compass-preferences-model": "^2.27.0",
+ "hadron-app-registry": "^9.2.3",
+ "mongodb-collection-model": "^5.23.0",
"mongodb-ns": "^2.4.2",
"numeral": "^2.0.6",
"react": "^17.0.2",
@@ -67,8 +67,8 @@
"redux-thunk": "^2.4.2"
},
"devDependencies": {
- "@mongodb-js/eslint-config-compass": "^1.1.4",
- "@mongodb-js/mocha-config-compass": "^1.3.10",
+ "@mongodb-js/eslint-config-compass": "^1.1.5",
+ "@mongodb-js/mocha-config-compass": "^1.4.0",
"@mongodb-js/prettier-config-compass": "^1.0.2",
"@mongodb-js/tsconfig-compass": "^1.0.4",
"@testing-library/react": "^12.1.5",
diff --git a/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.tsx b/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.tsx
index 2d5567b3f1a..ff013eaa3f4 100644
--- a/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.tsx
+++ b/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.tsx
@@ -58,17 +58,17 @@ const CollectionHeaderActions: React.FunctionComponent<
const {
readOnly: preferencesReadOnly,
enableShell,
- enableNewMultipleConnectionSystem,
+ enableMultipleConnectionSystem,
} = usePreferences([
'readOnly',
'enableShell',
- 'enableNewMultipleConnectionSystem',
+ 'enableMultipleConnectionSystem',
]);
const track = useTelemetry();
const { database, collection } = toNS(namespace);
- const showOpenShellButton = enableShell && enableNewMultipleConnectionSystem;
+ const showOpenShellButton = enableShell && enableMultipleConnectionSystem;
return (
;
+ component: HadronPluginComponent
;
}
type CollectionTabComponentsProviderValue = {
diff --git a/packages/compass-components/package.json b/packages/compass-components/package.json
index 4097c7ab54d..65606d0bb3e 100644
--- a/packages/compass-components/package.json
+++ b/packages/compass-components/package.json
@@ -1,6 +1,6 @@
{
"name": "@mongodb-js/compass-components",
- "version": "1.29.0",
+ "version": "1.29.1",
"description": "React Components used in Compass",
"license": "SSPL",
"main": "lib/index.js",
@@ -78,7 +78,7 @@
"@react-aria/visually-hidden": "^3.3.1",
"bson": "^6.7.0",
"focus-trap-react": "^9.0.2",
- "hadron-document": "^8.6.0",
+ "hadron-document": "^8.6.1",
"hadron-type-checker": "^7.2.2",
"is-electron-renderer": "^2.0.1",
"lodash": "^4.17.21",
@@ -92,8 +92,8 @@
},
"devDependencies": {
"@emotion/css": "^11.11.2",
- "@mongodb-js/eslint-config-compass": "^1.1.4",
- "@mongodb-js/mocha-config-compass": "^1.3.10",
+ "@mongodb-js/eslint-config-compass": "^1.1.5",
+ "@mongodb-js/mocha-config-compass": "^1.4.0",
"@mongodb-js/prettier-config-compass": "^1.0.2",
"@mongodb-js/tsconfig-compass": "^1.0.4",
"@testing-library/dom": "^8.20.1",
diff --git a/packages/compass-components/src/components/compass-components-provider.tsx b/packages/compass-components/src/components/compass-components-provider.tsx
index 58564cf58d8..18bac239d30 100644
--- a/packages/compass-components/src/components/compass-components-provider.tsx
+++ b/packages/compass-components/src/components/compass-components-provider.tsx
@@ -15,6 +15,7 @@ type CompassComponentsProviderProps = {
* value will be derived from the system settings
*/
darkMode?: boolean;
+ popoverPortalContainer?: HTMLElement;
/**
* Either React children or a render callback that will get the darkMode
* property passed as function properties
@@ -97,6 +98,7 @@ export const CompassComponentsProvider = ({
utmSource,
utmMedium,
stackedElementsZIndex,
+ popoverPortalContainer: _popoverPortalContainer,
...signalHooksProviderProps
}: CompassComponentsProviderProps) => {
const darkMode = useDarkMode(_darkMode);
@@ -107,7 +109,7 @@ export const CompassComponentsProvider = ({
// is literally no way around it with how leafygreen popover works and lucky
// for us, this will usually cause a state update only once
const [portalContainer, setPortalContainer] = useState(
- null
+ _popoverPortalContainer ?? null
);
const [scrollContainer, setScrollContainer] = useState(
null
diff --git a/packages/compass-components/src/components/error-boundary.tsx b/packages/compass-components/src/components/error-boundary.tsx
index 527044790c2..1c5d79a2795 100644
--- a/packages/compass-components/src/components/error-boundary.tsx
+++ b/packages/compass-components/src/components/error-boundary.tsx
@@ -18,7 +18,6 @@ type Props = {
className?: string;
displayName?: string;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
- children: React.ReactElement;
};
class ErrorBoundary extends React.Component {
diff --git a/packages/compass-components/src/components/workspace-tabs/tab.tsx b/packages/compass-components/src/components/workspace-tabs/tab.tsx
index 4db6b63c3cf..7b5369e8f20 100644
--- a/packages/compass-components/src/components/workspace-tabs/tab.tsx
+++ b/packages/compass-components/src/components/workspace-tabs/tab.tsx
@@ -47,6 +47,7 @@ const tabStyles = css({
boxShadow: 'inset -1px -1px 0 0 var(--workspace-tab-border-color)',
'&:hover': {
+ backgroundColor: 'inherit',
cursor: 'pointer',
zIndex: 1,
},
@@ -132,6 +133,7 @@ const selectedTabStyles = css({
boxShadow: 'inset -1px 0 0 0 var(--workspace-tab-border-color)',
'&:hover': {
+ backgroundColor: 'var(--workspace-tab-selected-background-color)',
cursor: 'default',
},
diff --git a/packages/compass-connection-import-export/package.json b/packages/compass-connection-import-export/package.json
index 3533c714200..76dc9ea3af1 100644
--- a/packages/compass-connection-import-export/package.json
+++ b/packages/compass-connection-import-export/package.json
@@ -14,7 +14,7 @@
"email": "compass@mongodb.com"
},
"homepage": "https://github.com/mongodb-js/compass",
- "version": "0.34.0",
+ "version": "0.35.0",
"repository": {
"type": "git",
"url": "https://github.com/mongodb-js/compass.git"
@@ -51,16 +51,16 @@
"reformat": "npm run eslint . -- --fix && npm run prettier -- --write ."
},
"dependencies": {
- "@mongodb-js/compass-components": "^1.29.0",
- "@mongodb-js/compass-connections": "^1.38.0",
- "@mongodb-js/connection-storage": "^0.17.0",
- "compass-preferences-model": "^2.26.0",
- "hadron-ipc": "^3.2.20",
+ "@mongodb-js/compass-components": "^1.29.1",
+ "@mongodb-js/compass-connections": "^1.39.0",
+ "@mongodb-js/connection-storage": "^0.18.0",
+ "compass-preferences-model": "^2.27.0",
+ "hadron-ipc": "^3.2.21",
"react": "^17.0.2"
},
"devDependencies": {
- "@mongodb-js/eslint-config-compass": "^1.1.4",
- "@mongodb-js/mocha-config-compass": "^1.3.10",
+ "@mongodb-js/eslint-config-compass": "^1.1.5",
+ "@mongodb-js/mocha-config-compass": "^1.4.0",
"@mongodb-js/prettier-config-compass": "^1.0.2",
"@mongodb-js/tsconfig-compass": "^1.0.4",
"@testing-library/react": "^12.1.5",
diff --git a/packages/compass-connection-import-export/src/components/import-modal.tsx b/packages/compass-connection-import-export/src/components/import-modal.tsx
index c096d12c5cd..68b89f9bc99 100644
--- a/packages/compass-connection-import-export/src/components/import-modal.tsx
+++ b/packages/compass-connection-import-export/src/components/import-modal.tsx
@@ -39,7 +39,7 @@ export function ImportConnectionsModal({
trackingProps?: Record;
}): React.ReactElement {
const multipleConnectionsEnabled = usePreference(
- 'enableNewMultipleConnectionSystem'
+ 'enableMultipleConnectionSystem'
);
const { openToast } = useToast('compass-connection-import-export');
const finish = useCallback(
diff --git a/packages/compass-connection-import-export/src/hooks/use-export-connections.spec.ts b/packages/compass-connection-import-export/src/hooks/use-export-connections.spec.ts
deleted file mode 100644
index 4dd614558ea..00000000000
--- a/packages/compass-connection-import-export/src/hooks/use-export-connections.spec.ts
+++ /dev/null
@@ -1,354 +0,0 @@
-import type React from 'react';
-import { expect } from 'chai';
-import sinon from 'sinon';
-import type {
- RenderResult,
- RenderHookResult,
-} from '@testing-library/react-hooks';
-import { renderHook, act } from '@testing-library/react-hooks';
-import { useExportConnections } from './use-export-connections';
-import type { ImportExportResult } from './common';
-import os from 'os';
-import path from 'path';
-import { promises as fs } from 'fs';
-import { PreferencesProvider } from 'compass-preferences-model/provider';
-import { createSandboxFromDefaultPreferences } from 'compass-preferences-model';
-import { createElement } from 'react';
-import {
- ConnectionStorageProvider,
- type ConnectionStorage,
- InMemoryConnectionStorage,
-} from '@mongodb-js/connection-storage/provider';
-import { useConnectionRepository } from '@mongodb-js/compass-connections/provider';
-
-type UseExportConnectionsProps = Parameters[0];
-type UseExportConnectionsResult = ReturnType;
-type UseConnectionRepositoryResult = ReturnType;
-type HookResults = {
- connectionRepository: UseConnectionRepositoryResult;
- exportConnections: UseExportConnectionsResult;
-};
-
-describe('useExportConnections', function () {
- let sandbox: sinon.SinonSandbox;
- let finish: sinon.SinonStub;
- let finishedPromise: Promise;
- let defaultProps: UseExportConnectionsProps;
- let renderHookResult: RenderHookResult<
- Partial,
- HookResults
- >;
- let result: RenderResult;
- let rerender: (props: Partial) => void;
- let tmpdir: string;
- let connectionStorage: ConnectionStorage;
-
- beforeEach(async function () {
- sandbox = sinon.createSandbox();
- finishedPromise = new Promise((resolve) => {
- finish = sinon.stub().callsFake(resolve);
- });
- defaultProps = {
- finish,
- open: true,
- trackingProps: { context: 'Tests' },
- };
- connectionStorage = new InMemoryConnectionStorage();
- const wrapper: React.FC = ({ children }) =>
- createElement(ConnectionStorageProvider, {
- value: connectionStorage,
- children,
- });
-
- renderHookResult = renderHook(
- (props: Partial = {}) => {
- return {
- connectionRepository: useConnectionRepository(),
- exportConnections: useExportConnections({
- ...defaultProps,
- ...props,
- }),
- };
- },
- {
- wrapper,
- }
- );
- ({ result, rerender } = renderHookResult);
- tmpdir = path.join(
- os.tmpdir(),
- `compass-export-connections-ui-${Date.now()}-${Math.floor(
- Math.random() * 1000
- )}`
- );
- await fs.mkdir(tmpdir, { recursive: true });
- });
-
- afterEach(async function () {
- sandbox.restore();
- await fs.rm(tmpdir, { recursive: true });
- });
-
- // Security-relevant test -- description is in the protect-connection-strings e2e test.
- it('sets removeSecrets if protectConnectionStrings is set', async function () {
- expect(result.current.exportConnections.state.removeSecrets).to.equal(
- false
- );
- act(() => {
- result.current.exportConnections.onChangeRemoveSecrets({
- target: { checked: true },
- } as any);
- });
- expect(result.current.exportConnections.state.removeSecrets).to.equal(true);
-
- const preferences = await createSandboxFromDefaultPreferences();
- await preferences.savePreferences({ protectConnectionStrings: true });
- const resultInProtectedMode = renderHook(
- () => {
- return useExportConnections(defaultProps);
- },
- {
- wrapper: ({ children }) =>
- createElement(PreferencesProvider, { children, value: preferences }),
- }
- ).result;
-
- expect(resultInProtectedMode.current.state.removeSecrets).to.equal(true);
- act(() => {
- resultInProtectedMode.current.onChangeRemoveSecrets({
- target: { checked: false },
- } as any);
- });
- expect(resultInProtectedMode.current.state.removeSecrets).to.equal(true);
- });
-
- it('responds to changes in the connectionList', async function () {
- expect(result.current.exportConnections.state.connectionList).to.deep.equal(
- []
- );
-
- await act(async () => {
- await result.current.connectionRepository.saveConnection({
- id: 'id1',
- connectionOptions: { connectionString: 'mongodb://localhost:2020' },
- favorite: {
- name: 'name1',
- },
- savedConnectionType: 'favorite',
- });
- });
- rerender({});
- expect(result.current.exportConnections.state.connectionList).to.deep.equal(
- [{ id: 'id1', name: 'name1', selected: true }]
- );
-
- act(() => {
- result.current.exportConnections.onChangeConnectionList([
- { id: 'id1', name: 'name1', selected: false },
- ]);
- });
- expect(result.current.exportConnections.state.connectionList).to.deep.equal(
- [{ id: 'id1', name: 'name1', selected: false }]
- );
-
- await act(async () => {
- await result.current.connectionRepository.saveConnection({
- id: 'id2',
- connectionOptions: { connectionString: 'mongodb://localhost:2020' },
- favorite: {
- name: 'name2',
- },
- savedConnectionType: 'favorite',
- });
- });
-
- expect(result.current.exportConnections.state.connectionList).to.deep.equal(
- [
- { id: 'id1', name: 'name1', selected: false },
- { id: 'id2', name: 'name2', selected: true },
- ]
- );
- });
-
- it('updates filename if changed', function () {
- act(() => {
- result.current.exportConnections.onChangeFilename('filename1234');
- });
- expect(result.current.exportConnections.state.filename).to.equal(
- 'filename1234'
- );
- });
-
- it('handles actual export', async function () {
- await act(async () => {
- await connectionStorage.save?.({
- connectionInfo: {
- id: 'id1',
- connectionOptions: {
- connectionString: 'mongodb://localhost:2020',
- },
- savedConnectionType: 'favorite',
- favorite: {
- name: 'name1',
- },
- },
- });
- await connectionStorage.save?.({
- connectionInfo: {
- id: 'id2',
- connectionOptions: {
- connectionString: 'mongodb://localhost:2021',
- },
- savedConnectionType: 'favorite',
- favorite: {
- name: 'name1',
- },
- },
- });
- });
- rerender({});
- act(() => {
- result.current.exportConnections.onChangeConnectionList([
- { id: 'id1', name: 'name1', selected: false },
- { id: 'id2', name: 'name2', selected: true },
- ]);
- });
-
- const filename = path.join(tmpdir, 'connections.json');
- const fileContents = '{"connections":[1,2,3]}';
- const exportConnectionStub = sandbox
- .stub(connectionStorage, 'exportConnections')
- .resolves(fileContents);
-
- act(() => {
- result.current.exportConnections.onChangeFilename(filename);
- result.current.exportConnections.onChangePassphrase('s3cr3t');
- });
-
- act(() => {
- result.current.exportConnections.onSubmit();
- });
-
- expect(await finishedPromise).to.equal('succeeded');
- expect(await fs.readFile(filename, 'utf8')).to.equal(fileContents);
- expect(exportConnectionStub).to.have.been.calledOnce;
- const arg = exportConnectionStub.firstCall.args[0];
- expect(arg?.options?.passphrase).to.equal('s3cr3t');
- expect(arg?.options?.filterConnectionIds).to.deep.equal(['id2']);
- expect(arg?.options?.trackingProps).to.deep.equal({ context: 'Tests' });
- expect(arg?.options?.removeSecrets).to.equal(false);
- });
-
- it('resets errors if filename changes', async function () {
- const filename = path.join(tmpdir, 'nonexistent', 'connections.json');
- const exportConnectionsStub = sandbox
- .stub(connectionStorage, 'exportConnections')
- .resolves('');
-
- act(() => {
- result.current.exportConnections.onChangeFilename(filename);
- });
-
- expect(result.current.exportConnections.state.inProgress).to.equal(false);
- act(() => {
- result.current.exportConnections.onSubmit();
- });
-
- expect(result.current.exportConnections.state.inProgress).to.equal(true);
- expect(result.current.exportConnections.state.error).to.equal('');
- await renderHookResult.waitForValueToChange(
- () => result.current.exportConnections.state.inProgress
- );
- expect(result.current.exportConnections.state.inProgress).to.equal(false);
- expect(result.current.exportConnections.state.error).to.include('ENOENT');
-
- expect(exportConnectionsStub).to.have.been.calledOnce;
- expect(finish).to.not.have.been.called;
-
- act(() => {
- result.current.exportConnections.onChangeFilename(filename + '-changed');
- });
-
- expect(result.current.exportConnections.state.error).to.equal('');
- });
-
- context('when multiple connections is enabled', function () {
- beforeEach(async function () {
- const preferences = await createSandboxFromDefaultPreferences();
- await preferences.savePreferences({
- enableNewMultipleConnectionSystem: true,
- });
- const wrapper: React.FC = ({ children }) =>
- createElement(PreferencesProvider, {
- value: preferences,
- children: createElement(ConnectionStorageProvider, {
- value: connectionStorage,
- children,
- }),
- });
- renderHookResult = renderHook(
- (props: Partial = {}) => {
- return {
- connectionRepository: useConnectionRepository(),
- exportConnections: useExportConnections({
- ...defaultProps,
- ...props,
- }),
- };
- },
- { wrapper }
- );
- ({ result, rerender } = renderHookResult);
- });
-
- it('includes also the non-favorites connections in the export list', async function () {
- expect(
- result.current.exportConnections.state.connectionList
- ).to.deep.equal([]);
-
- // expecting to include the non-favorite connections as well
- await act(async () => {
- await result.current.connectionRepository.saveConnection({
- id: 'id1',
- connectionOptions: { connectionString: 'mongodb://localhost:2020' },
- favorite: {
- name: 'name1',
- },
- savedConnectionType: 'recent',
- });
- });
-
- rerender({});
- expect(
- result.current.exportConnections.state.connectionList
- ).to.deep.equal([{ id: 'id1', name: 'name1', selected: true }]);
-
- act(() => {
- result.current.exportConnections.onChangeConnectionList([
- { id: 'id1', name: 'name1', selected: false },
- ]);
- });
- expect(
- result.current.exportConnections.state.connectionList
- ).to.deep.equal([{ id: 'id1', name: 'name1', selected: false }]);
-
- await act(async () => {
- await result.current.connectionRepository.saveConnection({
- id: 'id2',
- connectionOptions: { connectionString: 'mongodb://localhost:2020' },
- favorite: {
- name: 'name2',
- },
- savedConnectionType: 'recent',
- });
- });
-
- expect(
- result.current.exportConnections.state.connectionList
- ).to.deep.equal([
- { id: 'id1', name: 'name1', selected: false },
- { id: 'id2', name: 'name2', selected: true },
- ]);
- });
- });
-});
diff --git a/packages/compass-connection-import-export/src/hooks/use-export-connections.spec.tsx b/packages/compass-connection-import-export/src/hooks/use-export-connections.spec.tsx
new file mode 100644
index 00000000000..cfcff3677e6
--- /dev/null
+++ b/packages/compass-connection-import-export/src/hooks/use-export-connections.spec.tsx
@@ -0,0 +1,273 @@
+import { expect } from 'chai';
+import sinon from 'sinon';
+import { useExportConnections } from './use-export-connections';
+import type { ImportExportResult } from './common';
+import os from 'os';
+import path from 'path';
+import { promises as fs } from 'fs';
+import type { RenderConnectionsOptions } from '@mongodb-js/compass-connections/test';
+import {
+ renderHookWithConnections,
+ act,
+ cleanup,
+ createDefaultConnectionInfo,
+ waitFor,
+} from '@mongodb-js/compass-connections/test';
+
+describe('useExportConnections', function () {
+ let sandbox: sinon.SinonSandbox;
+ let finish: sinon.SinonStub;
+ let finishedPromise: Promise;
+ let tmpdir: string;
+
+ function renderUseExportConnectionsHook(
+ props?: Partial[0]>,
+ options?: RenderConnectionsOptions
+ ) {
+ return renderHookWithConnections(() => {
+ return useExportConnections({
+ finish,
+ open: true,
+ trackingProps: { context: 'Tests' },
+ ...props,
+ });
+ }, options);
+ }
+
+ beforeEach(async function () {
+ sandbox = sinon.createSandbox();
+ finishedPromise = new Promise((resolve) => {
+ finish = sandbox.stub().callsFake(resolve);
+ });
+ tmpdir = path.join(
+ os.tmpdir(),
+ `compass-export-connections-ui-${Date.now()}-${Math.floor(
+ Math.random() * 1000
+ )}`
+ );
+ await fs.mkdir(tmpdir, { recursive: true });
+ });
+
+ afterEach(async function () {
+ cleanup();
+ sandbox.restore();
+ await fs.rm(tmpdir, { recursive: true });
+ });
+
+ // Security-relevant test -- description is in the protect-connection-strings e2e test.
+ it('sets removeSecrets if protectConnectionStrings is set', function () {
+ const { result } = renderUseExportConnectionsHook();
+
+ expect(result.current.state.removeSecrets).to.equal(false);
+ act(() => {
+ result.current.onChangeRemoveSecrets({
+ target: { checked: true },
+ } as any);
+ });
+ expect(result.current.state.removeSecrets).to.equal(true);
+ cleanup();
+
+ const { result: resultInProtectedMode } = renderUseExportConnectionsHook(
+ {},
+ { preferences: { protectConnectionStrings: true } }
+ );
+
+ expect(resultInProtectedMode.current.state.removeSecrets).to.equal(true);
+ act(() => {
+ resultInProtectedMode.current.onChangeRemoveSecrets({
+ target: { checked: false },
+ } as any);
+ });
+ expect(resultInProtectedMode.current.state.removeSecrets).to.equal(true);
+ });
+
+ it('responds to changes in the connectionList', async function () {
+ const connectionInfo1 = createDefaultConnectionInfo();
+ const connectionInfo2 = createDefaultConnectionInfo();
+
+ const { result, connectionsStore, connectionStorage } =
+ renderUseExportConnectionsHook({}, { connections: [connectionInfo1] });
+
+ await act(async () => {
+ await connectionsStore.actions.saveEditedConnection({
+ ...connectionInfo1,
+ favorite: {
+ name: 'name1',
+ },
+ savedConnectionType: 'favorite',
+ });
+ });
+
+ expect(result.current.state.connectionList).to.deep.equal(
+ [
+ {
+ id: connectionInfo1.id,
+ name: 'name1',
+ selected: true,
+ },
+ ],
+ 'expected name of connection 1 to get updated after save'
+ );
+
+ act(() => {
+ result.current.onChangeConnectionList([
+ { id: connectionInfo1.id, name: 'name1', selected: false },
+ ]);
+ });
+
+ expect(result.current.state.connectionList).to.deep.equal(
+ [{ id: connectionInfo1.id, name: 'name1', selected: false }],
+ 'expected selected status of connection 1 to change'
+ );
+
+ await act(async () => {
+ await connectionStorage.save?.({
+ connectionInfo: {
+ ...connectionInfo2,
+ favorite: {
+ name: 'name2',
+ },
+ savedConnectionType: 'favorite',
+ },
+ });
+ await connectionsStore.actions.refreshConnections();
+ });
+
+ expect(result.current.state.connectionList).to.deep.equal([
+ { id: connectionInfo1.id, name: 'name1', selected: false },
+ { id: connectionInfo2.id, name: 'name2', selected: true },
+ ]);
+ });
+
+ it('updates filename if changed', function () {
+ const { result } = renderUseExportConnectionsHook();
+
+ act(() => {
+ result.current.onChangeFilename('filename1234');
+ });
+ expect(result.current.state.filename).to.equal('filename1234');
+ });
+
+ it('handles actual export', async function () {
+ const { result, connectionStorage } = renderUseExportConnectionsHook(
+ {},
+ {
+ connections: [
+ {
+ id: 'id1',
+ connectionOptions: {
+ connectionString: 'mongodb://localhost:2020',
+ },
+ savedConnectionType: 'favorite',
+ favorite: {
+ name: 'name1',
+ },
+ },
+ {
+ id: 'id2',
+ connectionOptions: {
+ connectionString: 'mongodb://localhost:2021',
+ },
+ savedConnectionType: 'favorite',
+ favorite: {
+ name: 'name1',
+ },
+ },
+ ],
+ }
+ );
+
+ act(() => {
+ result.current.onChangeConnectionList([
+ { id: 'id1', name: 'name1', selected: false },
+ { id: 'id2', name: 'name2', selected: true },
+ ]);
+ });
+
+ const filename = path.join(tmpdir, 'connections.json');
+ const fileContents = '{"connections":[1,2,3]}';
+ const exportConnectionStub = sandbox
+ .stub(connectionStorage, 'exportConnections')
+ .resolves(fileContents);
+
+ act(() => {
+ result.current.onChangeFilename(filename);
+ result.current.onChangePassphrase('s3cr3t');
+ });
+
+ act(() => {
+ result.current.onSubmit();
+ });
+
+ expect(await finishedPromise).to.equal('succeeded');
+ expect(await fs.readFile(filename, 'utf8')).to.equal(fileContents);
+ expect(exportConnectionStub).to.have.been.calledOnce;
+ const arg = exportConnectionStub.firstCall.firstArg;
+ expect(arg?.options?.passphrase).to.equal('s3cr3t');
+ expect(arg?.options?.filterConnectionIds).to.deep.equal(['id2']);
+ expect(arg?.options?.trackingProps).to.deep.equal({ context: 'Tests' });
+ expect(arg?.options?.removeSecrets).to.equal(false);
+ });
+
+ it('resets errors if filename changes', async function () {
+ const { result, connectionStorage } = renderUseExportConnectionsHook();
+
+ const filename = path.join(tmpdir, 'nonexistent', 'connections.json');
+ const exportConnectionsStub = sandbox
+ .stub(connectionStorage, 'exportConnections')
+ .resolves('');
+
+ act(() => {
+ result.current.onChangeFilename(filename);
+ });
+
+ expect(result.current.state.inProgress).to.equal(false);
+ act(() => {
+ result.current.onSubmit();
+ });
+
+ expect(result.current.state.inProgress).to.equal(true);
+ expect(result.current.state.error).to.equal('');
+ await waitFor(() => {
+ expect(result.current.state.inProgress).to.equal(false);
+ });
+ expect(result.current.state.error).to.include('ENOENT');
+
+ expect(exportConnectionsStub).to.have.been.calledOnce;
+ expect(finish).to.not.have.been.called;
+
+ act(() => {
+ result.current.onChangeFilename(filename + '-changed');
+ });
+
+ expect(result.current.state.error).to.equal('');
+ });
+
+ context('when multiple connections is enabled', function () {
+ it('includes also the non-favorites connections in the export list', function () {
+ const { result } = renderUseExportConnectionsHook(
+ {},
+ {
+ preferences: { enableMultipleConnectionSystem: true },
+ connections: [
+ {
+ id: 'id1',
+ connectionOptions: {
+ connectionString: 'mongodb://localhost:2020',
+ },
+ favorite: {
+ name: 'name1',
+ },
+ // expecting to include the non-favorite connections as well
+ savedConnectionType: 'recent',
+ },
+ ],
+ }
+ );
+
+ expect(result.current.state.connectionList).to.deep.equal([
+ { id: 'id1', name: 'name1', selected: true },
+ ]);
+ });
+ });
+});
diff --git a/packages/compass-connection-import-export/src/hooks/use-export-connections.ts b/packages/compass-connection-import-export/src/hooks/use-export-connections.ts
index ba2a057e58d..35a6a972660 100644
--- a/packages/compass-connection-import-export/src/hooks/use-export-connections.ts
+++ b/packages/compass-connection-import-export/src/hooks/use-export-connections.ts
@@ -56,7 +56,7 @@ export function useExportConnections({
state: ExportConnectionsState;
} {
const multipleConnectionsEnabled = usePreference(
- 'enableNewMultipleConnectionSystem'
+ 'enableMultipleConnectionSystem'
);
const { favoriteConnections, nonFavoriteConnections } =
useConnectionRepository();
@@ -80,25 +80,28 @@ export function useExportConnections({
}
const [state, setState] = useState(INITIAL_STATE);
- useEffect(() => setState(INITIAL_STATE), [open]);
+ useEffect(() => {
+ setState((prevState) => {
+ return {
+ // Reset the form state to initial when modal is open, but keep the list
+ ...INITIAL_STATE,
+ connectionList: prevState.connectionList,
+ };
+ });
+ }, [open]);
const { passphrase, filename, connectionList, removeSecrets } = state;
useEffect(() => {
// If `connectionsToExport` changes, update the list of connections
// that are displayed in our table.
- if (
- connectionsToExport.map(({ id }) => id).join(',') !==
- state.connectionList.map(({ id }) => id).join(',')
- ) {
- setState((prevState) => ({
- ...prevState,
- connectionList: connectionInfosToConnectionShortInfos(
- connectionsToExport,
- state.connectionList
- ),
- }));
- }
- }, [connectionsToExport, state.connectionList]);
+ setState((prevState) => ({
+ ...prevState,
+ connectionList: connectionInfosToConnectionShortInfos(
+ connectionsToExport,
+ prevState.connectionList
+ ),
+ }));
+ }, [connectionsToExport]);
const protectConnectionStrings = !!usePreference('protectConnectionStrings');
useEffect(() => {
diff --git a/packages/compass-connection-import-export/src/hooks/use-import-connections.spec.ts b/packages/compass-connection-import-export/src/hooks/use-import-connections.spec.ts
deleted file mode 100644
index 9f5d3105a91..00000000000
--- a/packages/compass-connection-import-export/src/hooks/use-import-connections.spec.ts
+++ /dev/null
@@ -1,462 +0,0 @@
-import React from 'react';
-import { expect } from 'chai';
-import sinon from 'sinon';
-import type {
- RenderResult,
- RenderHookResult,
-} from '@testing-library/react-hooks';
-import { renderHook, act } from '@testing-library/react-hooks';
-import { useImportConnections } from './use-import-connections';
-import type { ImportExportResult } from './common';
-import os from 'os';
-import path from 'path';
-import { promises as fs } from 'fs';
-import {
- type ConnectionInfo,
- type ConnectionStorage,
- InMemoryConnectionStorage,
-} from '@mongodb-js/connection-storage/provider';
-import { ConnectionStorageProvider } from '@mongodb-js/connection-storage/provider';
-import { useConnectionRepository } from '@mongodb-js/compass-connections/provider';
-import { createSandboxFromDefaultPreferences } from 'compass-preferences-model';
-import { PreferencesProvider } from 'compass-preferences-model/provider';
-
-type UseImportConnectionsProps = Parameters[0];
-type UseImportConnectionsResult = ReturnType;
-type UseConnectionRepositoryResult = ReturnType;
-type HookResults = {
- connectionRepository: UseConnectionRepositoryResult;
- importConnections: UseImportConnectionsResult;
-};
-const exampleFileContents = '{"a":"b"}';
-
-describe('useImportConnections', function () {
- let sandbox: sinon.SinonSandbox;
- let finish: sinon.SinonStub;
- let finishedPromise: Promise;
- let defaultProps: UseImportConnectionsProps;
- let renderHookResult: RenderHookResult<
- Partial,
- HookResults
- >;
- let result: RenderResult;
- let rerender: (props: Partial) => void;
- let tmpdir: string;
- let exampleFile: string;
- let connectionStorage: ConnectionStorage;
-
- beforeEach(async function () {
- sandbox = sinon.createSandbox();
- finishedPromise = new Promise((resolve) => {
- finish = sinon.stub().callsFake(resolve);
- });
- defaultProps = {
- finish,
- open: true,
- trackingProps: { context: 'Tests' },
- };
- connectionStorage = new InMemoryConnectionStorage();
- const wrapper: React.FC = ({ children }) =>
- React.createElement(ConnectionStorageProvider, {
- value: connectionStorage,
- children,
- });
- renderHookResult = renderHook(
- (props: Partial = {}) => {
- return {
- connectionRepository: useConnectionRepository(),
- importConnections: useImportConnections({
- ...defaultProps,
- ...props,
- }),
- };
- },
- { wrapper }
- );
- ({ result, rerender } = renderHookResult);
- tmpdir = path.join(
- os.tmpdir(),
- `compass-export-connections-ui-${Date.now()}-${Math.floor(
- Math.random() * 1000
- )}`
- );
- await fs.mkdir(tmpdir, { recursive: true });
- exampleFile = path.join(tmpdir, 'connections.json');
- await fs.writeFile(exampleFile, exampleFileContents);
- });
-
- afterEach(async function () {
- sandbox.restore();
- await fs.rm(tmpdir, { recursive: true });
- });
-
- it('updates filename if changed', async function () {
- const deserializeStub = sandbox
- .stub(connectionStorage, 'deserializeConnections')
- .callsFake(function ({
- content,
- options,
- }: {
- content: string;
- options: any;
- }) {
- expect(content).to.equal(exampleFileContents);
- expect(options.passphrase).to.equal('');
- return Promise.resolve([
- {
- id: 'id1',
- favorite: { name: 'name1' },
- } as ConnectionInfo,
- ]);
- });
-
- act(() => {
- result.current.importConnections.onChangeFilename(exampleFile);
- });
- expect(result.current.importConnections.state.filename).to.equal(
- exampleFile
- );
- expect(result.current.importConnections.state.error).to.equal('');
- expect(result.current.importConnections.state.connectionList).to.deep.equal(
- []
- );
- await renderHookResult.waitForValueToChange(
- () => result.current.importConnections.state.connectionList.length
- );
-
- expect(deserializeStub).to.have.been.calledOnce;
- expect(result.current.importConnections.state.connectionList).to.deep.equal(
- [
- {
- id: 'id1',
- name: 'name1',
- selected: true,
- isExistingConnection: false,
- },
- ]
- );
- });
-
- it('updates passphrase if changed', async function () {
- sandbox
- .stub(connectionStorage, 'deserializeConnections')
- .onFirstCall()
- .callsFake(function ({
- content,
- options,
- }: {
- content: string;
- options: any;
- }) {
- expect(content).to.equal(exampleFileContents);
- expect(options.passphrase).to.equal('wrong');
- throw Object.assign(new Error('wrong password'), {
- passphraseRequired: true,
- });
- })
- .onSecondCall()
- .callsFake(function ({
- content,
- options,
- }: {
- content: string;
- options: any;
- }) {
- expect(content).to.equal(exampleFileContents);
- expect(options.passphrase).to.equal('s3cr3t');
- return Promise.resolve([
- {
- id: 'id1',
- favorite: { name: 'name1' },
- } as ConnectionInfo,
- ]);
- });
-
- act(() => {
- result.current.importConnections.onChangeFilename(exampleFile);
- result.current.importConnections.onChangePassphrase('wrong');
- });
- expect(result.current.importConnections.state.passphrase).to.equal('wrong');
-
- expect(result.current.importConnections.state.error).to.equal('');
- await renderHookResult.waitForValueToChange(
- () => result.current.importConnections.state.error
- );
- expect(result.current.importConnections.state.error).to.equal(
- 'wrong password'
- );
- expect(result.current.importConnections.state.passphraseRequired).to.equal(
- true
- );
-
- act(() => {
- result.current.importConnections.onChangePassphrase('s3cr3t');
- });
- expect(result.current.importConnections.state.passphrase).to.equal(
- 's3cr3t'
- );
-
- await renderHookResult.waitForValueToChange(
- () => result.current.importConnections.state.error
- );
-
- expect(result.current.importConnections.state.error).to.equal('');
- expect(result.current.importConnections.state.passphraseRequired).to.equal(
- true
- );
- expect(
- result.current.importConnections.state.connectionList
- ).to.have.lengthOf(1);
- });
-
- it('does not select existing favorites by default', async function () {
- sandbox
- .stub(connectionStorage, 'deserializeConnections')
- .callsFake(({ content, options }: { content: string; options: any }) => {
- expect(content).to.equal(exampleFileContents);
- expect(options.passphrase).to.equal('');
- return Promise.resolve([
- {
- id: 'id1',
- favorite: { name: 'name1' },
- },
- {
- id: 'id2',
- favorite: { name: 'name2' },
- },
- ] as ConnectionInfo[]);
- });
-
- await act(async () => {
- await result.current.connectionRepository.saveConnection({
- id: 'id1',
- connectionOptions: { connectionString: 'mongodb://localhost:2020' },
- favorite: {
- name: 'name1',
- },
- savedConnectionType: 'favorite',
- });
- });
-
- rerender({});
- act(() => {
- result.current.importConnections.onChangeFilename(exampleFile);
- });
-
- await renderHookResult.waitForValueToChange(
- () => result.current.importConnections.state.connectionList.length
- );
- expect(result.current.importConnections.state.connectionList).to.deep.equal(
- [
- {
- id: 'id1',
- name: 'name1',
- selected: false,
- isExistingConnection: true,
- },
- {
- id: 'id2',
- name: 'name2',
- selected: true,
- isExistingConnection: false,
- },
- ]
- );
-
- await act(async () => {
- await result.current.connectionRepository.saveConnection({
- id: 'id2',
- connectionOptions: { connectionString: 'mongodb://localhost:2020' },
- favorite: {
- name: 'name2',
- },
- savedConnectionType: 'favorite',
- });
- });
-
- rerender({});
- expect(result.current.importConnections.state.connectionList).to.deep.equal(
- [
- {
- id: 'id1',
- name: 'name1',
- selected: false,
- isExistingConnection: true,
- },
- {
- id: 'id2',
- name: 'name2',
- selected: true,
- isExistingConnection: true,
- },
- ]
- );
- });
-
- it('handles actual import', async function () {
- const connections = [
- {
- id: 'id1',
- favorite: { name: 'name1' },
- },
- {
- id: 'id2',
- favorite: { name: 'name2' },
- },
- ];
- sandbox
- .stub(connectionStorage, 'deserializeConnections')
- .resolves(connections as ConnectionInfo[]);
- const importConnectionsStub = sandbox
- .stub(connectionStorage, 'importConnections')
- .callsFake(({ content }: { content: string }) => {
- expect(content).to.equal(exampleFileContents);
- return Promise.resolve();
- });
- act(() => {
- result.current.importConnections.onChangeFilename(exampleFile);
- });
- await renderHookResult.waitForValueToChange(
- () => result.current.importConnections.state.fileContents
- );
-
- act(() => {
- result.current.importConnections.onChangeConnectionList([
- { id: 'id1', name: 'name1', selected: false },
- { id: 'id2', name: 'name2', selected: true },
- ]);
- });
-
- act(() => {
- result.current.importConnections.onSubmit();
- });
-
- expect(await finishedPromise).to.equal('succeeded');
- expect(importConnectionsStub).to.have.been.calledOnce;
- const arg = importConnectionsStub.firstCall.args[0];
- expect(arg?.options?.trackingProps).to.deep.equal({
- context: 'Tests',
- connection_ids: ['id2'],
- });
- expect(arg?.options?.filterConnectionIds).to.deep.equal(['id2']);
- });
-
- context('when multiple connections is enabled', function () {
- beforeEach(async function () {
- const preferences = await createSandboxFromDefaultPreferences();
- await preferences.savePreferences({
- enableNewMultipleConnectionSystem: true,
- });
- const wrapper: React.FC = ({ children }) =>
- React.createElement(PreferencesProvider, {
- value: preferences,
- children: React.createElement(ConnectionStorageProvider, {
- value: connectionStorage,
- children,
- }),
- });
- renderHookResult = renderHook(
- (props: Partial = {}) => {
- return {
- connectionRepository: useConnectionRepository(),
- importConnections: useImportConnections({
- ...defaultProps,
- ...props,
- }),
- };
- },
- { wrapper }
- );
- ({ result, rerender } = renderHookResult);
- });
- it('does not select existing connections (including non-favorites) by default', async function () {
- sandbox
- .stub(connectionStorage, 'deserializeConnections')
- .callsFake(
- ({ content, options }: { content: string; options: any }) => {
- expect(content).to.equal(exampleFileContents);
- expect(options.passphrase).to.equal('');
- // we're expecting both these non-favorite connections to be taken into
- // account when performing the diff
- return Promise.resolve([
- {
- id: 'id1',
- favorite: { name: 'name1' },
- savedConnectionType: 'recent',
- },
- {
- id: 'id2',
- favorite: { name: 'name2' },
- savedConnectionType: 'recent',
- },
- ] as ConnectionInfo[]);
- }
- );
-
- await act(async () => {
- await result.current.connectionRepository.saveConnection({
- id: 'id1',
- connectionOptions: { connectionString: 'mongodb://localhost:2020' },
- favorite: {
- name: 'name1',
- },
- savedConnectionType: 'recent',
- });
- });
-
- rerender({});
- act(() => {
- result.current.importConnections.onChangeFilename(exampleFile);
- });
-
- await renderHookResult.waitForValueToChange(
- () => result.current.importConnections.state.connectionList.length
- );
- expect(
- result.current.importConnections.state.connectionList
- ).to.deep.equal([
- {
- id: 'id1',
- name: 'name1',
- selected: false,
- isExistingConnection: true,
- },
- {
- id: 'id2',
- name: 'name2',
- selected: true,
- isExistingConnection: false,
- },
- ]);
-
- await act(async () => {
- await result.current.connectionRepository.saveConnection({
- id: 'id2',
- connectionOptions: { connectionString: 'mongodb://localhost:2020' },
- favorite: {
- name: 'name2',
- },
- savedConnectionType: 'recent',
- });
- });
-
- rerender({});
- expect(
- result.current.importConnections.state.connectionList
- ).to.deep.equal([
- {
- id: 'id1',
- name: 'name1',
- selected: false,
- isExistingConnection: true,
- },
- {
- id: 'id2',
- name: 'name2',
- selected: true,
- isExistingConnection: true,
- },
- ]);
- });
- });
-});
diff --git a/packages/compass-connection-import-export/src/hooks/use-import-connections.spec.tsx b/packages/compass-connection-import-export/src/hooks/use-import-connections.spec.tsx
new file mode 100644
index 00000000000..6b47e31c8bd
--- /dev/null
+++ b/packages/compass-connection-import-export/src/hooks/use-import-connections.spec.tsx
@@ -0,0 +1,368 @@
+import { expect } from 'chai';
+import sinon from 'sinon';
+import { useImportConnections } from './use-import-connections';
+import type { ImportExportResult } from './common';
+import os from 'os';
+import path from 'path';
+import { promises as fs } from 'fs';
+import { type ConnectionInfo } from '@mongodb-js/connection-storage/provider';
+import type { RenderConnectionsOptions } from '@mongodb-js/compass-connections/test';
+import {
+ renderHookWithConnections,
+ waitFor,
+ act,
+} from '@mongodb-js/compass-connections/test';
+
+const exampleFileContents = '{"a":"b"}';
+
+describe('useImportConnections', function () {
+ let sandbox: sinon.SinonSandbox;
+ let finish: sinon.SinonStub;
+ let finishedPromise: Promise;
+ let tmpdir: string;
+ let exampleFile: string;
+
+ function renderUseImportConnectionsHook(
+ props?: Partial[0]>,
+ options?: RenderConnectionsOptions
+ ) {
+ return renderHookWithConnections(() => {
+ return useImportConnections({
+ finish,
+ open: true,
+ trackingProps: { context: 'Tests' },
+ ...props,
+ });
+ }, options);
+ }
+
+ beforeEach(async function () {
+ sandbox = sinon.createSandbox();
+ finishedPromise = new Promise((resolve) => {
+ finish = sinon.stub().callsFake(resolve);
+ });
+ tmpdir = path.join(
+ os.tmpdir(),
+ `compass-export-connections-ui-${Date.now()}-${Math.floor(
+ Math.random() * 1000
+ )}`
+ );
+ await fs.mkdir(tmpdir, { recursive: true });
+ exampleFile = path.join(tmpdir, 'connections.json');
+ await fs.writeFile(exampleFile, exampleFileContents);
+ });
+
+ afterEach(async function () {
+ sandbox.restore();
+ await fs.rm(tmpdir, { recursive: true });
+ });
+
+ it('updates filename if changed', async function () {
+ const { result, connectionStorage } = renderUseImportConnectionsHook();
+
+ const deserializeStub = sandbox
+ .stub(connectionStorage, 'deserializeConnections')
+ .callsFake(function ({
+ content,
+ options,
+ }: {
+ content: string;
+ options: any;
+ }) {
+ expect(content).to.equal(exampleFileContents);
+ expect(options.passphrase).to.equal('');
+ return Promise.resolve([
+ {
+ id: 'id1',
+ favorite: { name: 'name1' },
+ } as ConnectionInfo,
+ ]);
+ });
+
+ act(() => {
+ result.current.onChangeFilename(exampleFile);
+ });
+ expect(result.current.state.filename).to.equal(exampleFile);
+ expect(result.current.state.error).to.equal('');
+ expect(result.current.state.connectionList).to.deep.equal([]);
+ await waitFor(() => {
+ expect(result.current.state.connectionList).to.deep.equal([
+ {
+ id: 'id1',
+ name: 'name1',
+ selected: true,
+ isExistingConnection: false,
+ },
+ ]);
+ });
+ expect(deserializeStub).to.have.been.calledOnce;
+ });
+
+ it('updates passphrase if changed', async function () {
+ const { result, connectionStorage } = renderUseImportConnectionsHook();
+
+ sandbox
+ .stub(connectionStorage, 'deserializeConnections')
+ .onFirstCall()
+ .callsFake(function ({
+ content,
+ options,
+ }: {
+ content: string;
+ options: any;
+ }) {
+ expect(content).to.equal(exampleFileContents);
+ expect(options.passphrase).to.equal('wrong');
+ throw Object.assign(new Error('wrong password'), {
+ passphraseRequired: true,
+ });
+ })
+ .onSecondCall()
+ .callsFake(function ({
+ content,
+ options,
+ }: {
+ content: string;
+ options: any;
+ }) {
+ expect(content).to.equal(exampleFileContents);
+ expect(options.passphrase).to.equal('s3cr3t');
+ return Promise.resolve([
+ {
+ id: 'id1',
+ favorite: { name: 'name1' },
+ } as ConnectionInfo,
+ ]);
+ });
+
+ act(() => {
+ result.current.onChangeFilename(exampleFile);
+ result.current.onChangePassphrase('wrong');
+ });
+ expect(result.current.state.passphrase).to.equal('wrong');
+
+ expect(result.current.state.error).to.equal('');
+ await waitFor(() => {
+ expect(result.current.state.error).to.equal('wrong password');
+ });
+ expect(result.current.state.passphraseRequired).to.equal(true);
+
+ act(() => {
+ result.current.onChangePassphrase('s3cr3t');
+ });
+ expect(result.current.state.passphrase).to.equal('s3cr3t');
+
+ await waitFor(() => {
+ expect(result.current.state.error).to.equal('');
+ });
+
+ expect(result.current.state.passphraseRequired).to.equal(true);
+ expect(result.current.state.connectionList).to.have.lengthOf(1);
+ });
+
+ it('does not select existing favorites by default', async function () {
+ const { result, connectionStorage, connectionsStore } =
+ renderUseImportConnectionsHook(
+ {},
+ {
+ connections: [
+ {
+ id: 'id1',
+ connectionOptions: {
+ connectionString: 'mongodb://localhost:2020',
+ },
+ favorite: {
+ name: 'name1',
+ },
+ savedConnectionType: 'favorite',
+ },
+ ],
+ }
+ );
+
+ sandbox
+ .stub(connectionStorage, 'deserializeConnections')
+ .callsFake(({ content, options }: { content: string; options: any }) => {
+ expect(content).to.equal(exampleFileContents);
+ expect(options.passphrase).to.equal('');
+ return Promise.resolve([
+ {
+ id: 'id1',
+ favorite: { name: 'name1' },
+ },
+ {
+ id: 'id2',
+ favorite: { name: 'name2' },
+ },
+ ] as ConnectionInfo[]);
+ });
+
+ act(() => {
+ result.current.onChangeFilename(exampleFile);
+ });
+
+ await waitFor(() => {
+ expect(result.current.state.connectionList).to.deep.equal([
+ {
+ id: 'id1',
+ name: 'name1',
+ selected: false,
+ isExistingConnection: true,
+ },
+ {
+ id: 'id2',
+ name: 'name2',
+ selected: true,
+ isExistingConnection: false,
+ },
+ ]);
+ });
+
+ await connectionStorage.save?.({
+ connectionInfo: {
+ id: 'id2',
+ connectionOptions: { connectionString: 'mongodb://localhost:2020' },
+ favorite: {
+ name: 'name2',
+ },
+ savedConnectionType: 'favorite',
+ },
+ });
+
+ await connectionsStore.actions.refreshConnections();
+
+ await waitFor(() => {
+ expect(result.current.state.connectionList).to.deep.equal([
+ {
+ id: 'id1',
+ name: 'name1',
+ selected: false,
+ isExistingConnection: true,
+ },
+ {
+ id: 'id2',
+ name: 'name2',
+ selected: true,
+ isExistingConnection: true,
+ },
+ ]);
+ });
+ });
+
+ it('handles actual import', async function () {
+ const { result, connectionStorage } = renderUseImportConnectionsHook();
+
+ const connections = [
+ {
+ id: 'id1',
+ favorite: { name: 'name1' },
+ },
+ {
+ id: 'id2',
+ favorite: { name: 'name2' },
+ },
+ ];
+ sandbox
+ .stub(connectionStorage, 'deserializeConnections')
+ .resolves(connections as any);
+ const importConnectionsStub = sandbox
+ .stub(connectionStorage, 'importConnections')
+ .callsFake(({ content }: { content: string }) => {
+ expect(content).to.equal(exampleFileContents);
+ return Promise.resolve();
+ });
+ act(() => {
+ result.current.onChangeFilename(exampleFile);
+ });
+ await waitFor(() => {
+ expect(result.current.state.fileContents).to.eq(exampleFileContents);
+ });
+
+ act(() => {
+ result.current.onChangeConnectionList([
+ { id: 'id1', name: 'name1', selected: false },
+ { id: 'id2', name: 'name2', selected: true },
+ ]);
+ });
+
+ act(() => {
+ result.current.onSubmit();
+ });
+
+ expect(await finishedPromise).to.equal('succeeded');
+ expect(importConnectionsStub).to.have.been.calledOnce;
+ const arg = importConnectionsStub.firstCall.args[0];
+ expect(arg?.options?.trackingProps).to.deep.equal({
+ context: 'Tests',
+ connection_ids: ['id2'],
+ });
+ expect(arg?.options?.filterConnectionIds).to.deep.equal(['id2']);
+ });
+
+ context('when multiple connections is enabled', function () {
+ it('does not select existing connections (including non-favorites) by default', async function () {
+ const { result, connectionStorage } = renderUseImportConnectionsHook(
+ {},
+ {
+ preferences: { enableMultipleConnectionSystem: true },
+ connections: [
+ {
+ id: 'id1',
+ connectionOptions: {
+ connectionString: 'mongodb://localhost:2020',
+ },
+ favorite: {
+ name: 'name1',
+ },
+ savedConnectionType: 'recent',
+ },
+ ],
+ }
+ );
+
+ sandbox
+ .stub(connectionStorage, 'deserializeConnections')
+ .callsFake(
+ ({ content, options }: { content: string; options: any }) => {
+ expect(content).to.equal(exampleFileContents);
+ expect(options.passphrase).to.equal('');
+ // we're expecting both these non-favorite connections to be taken into
+ // account when performing the diff
+ return Promise.resolve([
+ {
+ id: 'id1',
+ favorite: { name: 'name1' },
+ savedConnectionType: 'recent',
+ },
+ {
+ id: 'id2',
+ favorite: { name: 'name2' },
+ savedConnectionType: 'recent',
+ },
+ ] as ConnectionInfo[]);
+ }
+ );
+
+ act(() => {
+ result.current.onChangeFilename(exampleFile);
+ });
+
+ await waitFor(() => {
+ expect(result.current.state.connectionList).to.deep.equal([
+ {
+ id: 'id1',
+ name: 'name1',
+ selected: false,
+ isExistingConnection: true,
+ },
+ {
+ id: 'id2',
+ name: 'name2',
+ selected: true,
+ isExistingConnection: false,
+ },
+ ]);
+ });
+ });
+ });
+});
diff --git a/packages/compass-connection-import-export/src/hooks/use-import-connections.ts b/packages/compass-connection-import-export/src/hooks/use-import-connections.ts
index afaa46a5f6c..4685d422c84 100644
--- a/packages/compass-connection-import-export/src/hooks/use-import-connections.ts
+++ b/packages/compass-connection-import-export/src/hooks/use-import-connections.ts
@@ -13,7 +13,10 @@ import type {
ConnectionShortInfo,
CommonImportExportState,
} from './common';
-import { useConnectionRepository } from '@mongodb-js/compass-connections/provider';
+import {
+ useConnectionActions,
+ useConnectionRepository,
+} from '@mongodb-js/compass-connections/provider';
import { usePreference } from 'compass-preferences-model/provider';
type ConnectionImportInfo = ConnectionShortInfo & {
@@ -101,10 +104,11 @@ export function useImportConnections({
state: ImportConnectionsState;
} {
const multipleConnectionsEnabled = usePreference(
- 'enableNewMultipleConnectionSystem'
+ 'enableMultipleConnectionSystem'
);
const { favoriteConnections, nonFavoriteConnections } =
useConnectionRepository();
+ const { importConnections } = useConnectionActions();
const existingConnections = useMemo(() => {
// in case of multiple connections all the connections are saved (that used
// to be favorites in the single connection world) so we need to account for
@@ -116,18 +120,24 @@ export function useImportConnections({
}
}, [multipleConnectionsEnabled, favoriteConnections, nonFavoriteConnections]);
const connectionStorage = useConnectionStorageContext();
- const importConnectionsImpl =
- connectionStorage.importConnections?.bind(connectionStorage);
const deserializeConnectionsImpl =
connectionStorage.deserializeConnections?.bind(connectionStorage);
- if (!importConnectionsImpl || !deserializeConnectionsImpl) {
+ if (!deserializeConnectionsImpl) {
throw new Error(
'Import Connections feature requires the provided ConnectionStorage to implement importConnections and deserializeConnections'
);
}
const [state, setState] = useState(INITIAL_STATE);
- useEffect(() => setState(INITIAL_STATE), [open]);
+ useEffect(() => {
+ // Reset the form state to initial when modal is open, but keep the list
+ setState((prevState) => {
+ return {
+ ...INITIAL_STATE,
+ connectionList: prevState.connectionList,
+ };
+ });
+ }, [open]);
const { passphrase, filename, fileContents, connectionList } = state;
const existingConnectionIds = existingConnections.map(({ id }) => id);
@@ -153,7 +163,7 @@ export function useImportConnections({
.filter((x) => x.selected)
.map((x) => x.id);
try {
- await importConnectionsImpl({
+ await importConnections({
content: fileContents,
options: {
passphrase,
diff --git a/packages/compass-connections-navigation/package.json b/packages/compass-connections-navigation/package.json
index 2f52059b82a..a50ec07b35e 100644
--- a/packages/compass-connections-navigation/package.json
+++ b/packages/compass-connections-navigation/package.json
@@ -13,7 +13,7 @@
"email": "compass@mongodb.com"
},
"homepage": "https://github.com/mongodb-js/compass",
- "version": "1.37.0",
+ "version": "1.38.0",
"repository": {
"type": "git",
"url": "https://github.com/mongodb-js/compass.git"
@@ -23,9 +23,11 @@
],
"license": "SSPL",
"main": "dist/index.js",
+ "types": "dist/index.d.ts",
"compass:main": "src/index.ts",
"exports": {
- "require": "./dist/index.js"
+ "require": "./dist/index.js",
+ "types": "./dist/index.d.ts"
},
"compass:exports": {
".": "./src/index.ts"
@@ -47,20 +49,20 @@
"reformat": "npm run eslint . -- --fix && npm run prettier -- --write ."
},
"dependencies": {
- "@mongodb-js/compass-connections": "^1.38.0",
- "@mongodb-js/compass-components": "^1.29.0",
- "@mongodb-js/connection-info": "^0.5.3",
- "@mongodb-js/connection-form": "^1.36.0",
- "@mongodb-js/compass-workspaces": "^0.19.0",
- "compass-preferences-model": "^2.26.0",
+ "@mongodb-js/compass-connections": "^1.39.0",
+ "@mongodb-js/compass-components": "^1.29.1",
+ "@mongodb-js/connection-info": "^0.6.0",
+ "@mongodb-js/connection-form": "^1.37.0",
+ "@mongodb-js/compass-workspaces": "^0.20.0",
+ "compass-preferences-model": "^2.27.0",
"mongodb-build-info": "^1.7.2",
"react": "^17.0.2",
"react-virtualized-auto-sizer": "^1.0.6",
"react-window": "^1.8.6"
},
"devDependencies": {
- "@mongodb-js/eslint-config-compass": "^1.1.4",
- "@mongodb-js/mocha-config-compass": "^1.3.10",
+ "@mongodb-js/eslint-config-compass": "^1.1.5",
+ "@mongodb-js/mocha-config-compass": "^1.4.0",
"@mongodb-js/prettier-config-compass": "^1.0.2",
"@mongodb-js/tsconfig-compass": "^1.0.4",
"@testing-library/react": "^12.1.5",
diff --git a/packages/compass-connections-navigation/src/connections-navigation-tree.spec.tsx b/packages/compass-connections-navigation/src/connections-navigation-tree.spec.tsx
index 7362bb4ea33..971d2620901 100644
--- a/packages/compass-connections-navigation/src/connections-navigation-tree.spec.tsx
+++ b/packages/compass-connections-navigation/src/connections-navigation-tree.spec.tsx
@@ -140,7 +140,7 @@ describe('ConnectionsNavigationTree', function () {
preferences = await createSandboxFromDefaultPreferences();
await preferences.savePreferences({
enableRenameCollectionModal: true,
- enableNewMultipleConnectionSystem: true,
+ enableMultipleConnectionSystem: true,
...preferencesOverrides,
});
return render(
@@ -659,7 +659,7 @@ describe('ConnectionsNavigationTree', function () {
preferences = await createSandboxFromDefaultPreferences();
await preferences.savePreferences({
enableRenameCollectionModal: true,
- enableNewMultipleConnectionSystem: true,
+ enableMultipleConnectionSystem: true,
});
});
diff --git a/packages/compass-connections-navigation/src/connections-navigation-tree.tsx b/packages/compass-connections-navigation/src/connections-navigation-tree.tsx
index 470d4a9fe66..98937558333 100644
--- a/packages/compass-connections-navigation/src/connections-navigation-tree.tsx
+++ b/packages/compass-connections-navigation/src/connections-navigation-tree.tsx
@@ -27,7 +27,6 @@ import {
databaseItemActions,
notConnectedConnectionItemActions,
} from './item-actions';
-import { ConnectionStatus } from '@mongodb-js/compass-connections/provider';
const MCContainer = css({
display: 'flex',
@@ -60,9 +59,7 @@ const ConnectionsNavigationTree: React.FunctionComponent<
}) => {
const preferencesShellEnabled = usePreference('enableShell');
const preferencesReadOnly = usePreference('readOnly');
- const isSingleConnection = !usePreference(
- 'enableNewMultipleConnectionSystem'
- );
+ const isSingleConnection = !usePreference('enableMultipleConnectionSystem');
const isRenameCollectionEnabled = usePreference(
'enableRenameCollectionModal'
);
@@ -88,12 +85,9 @@ const ConnectionsNavigationTree: React.FunctionComponent<
const onDefaultAction: OnDefaultAction = useCallback(
(item, evt) => {
if (item.type === 'connection') {
- if (item.connectionStatus === ConnectionStatus.Connected) {
+ if (item.connectionStatus === 'connected') {
onItemAction(item, 'select-connection');
- } else if (
- item.connectionStatus === ConnectionStatus.Disconnected ||
- item.connectionStatus === ConnectionStatus.Failed
- ) {
+ } else {
onItemAction(item, 'connection-connect');
}
} else if (item.type === 'database') {
@@ -173,7 +167,7 @@ const ConnectionsNavigationTree: React.FunctionComponent<
actions: [],
};
case 'connection': {
- if (item.connectionStatus === ConnectionStatus.Connected) {
+ if (item.connectionStatus === 'connected') {
const actions = connectedConnectionItemActions({
hasWriteActionsDisabled: item.hasWriteActionsDisabled,
isShellEnabled: item.isShellEnabled,
diff --git a/packages/compass-connections-navigation/src/navigation-item.tsx b/packages/compass-connections-navigation/src/navigation-item.tsx
index e10469f84e8..e655bbe04f9 100644
--- a/packages/compass-connections-navigation/src/navigation-item.tsx
+++ b/packages/compass-connections-navigation/src/navigation-item.tsx
@@ -257,8 +257,7 @@ export function NavigationItem({
dataAttributes={itemDataProps}
isExpandVisible={item.isExpandable}
isExpandDisabled={
- item.type === 'connection' &&
- item.connectionStatus === 'disconnected'
+ item.type === 'connection' && item.connectionStatus !== 'connected'
}
onExpand={(isExpanded: boolean) => {
onItemExpand(item, isExpanded);
diff --git a/packages/compass-connections-navigation/src/placeholder.tsx b/packages/compass-connections-navigation/src/placeholder.tsx
index 6d75bbc9665..2b6523a29ad 100644
--- a/packages/compass-connections-navigation/src/placeholder.tsx
+++ b/packages/compass-connections-navigation/src/placeholder.tsx
@@ -23,9 +23,7 @@ export const PlaceholderItem: React.FunctionComponent<{
level: number;
style?: CSSProperties;
}> = ({ level, style }) => {
- const isSingleConnection = !usePreference(
- 'enableNewMultipleConnectionSystem'
- );
+ const isSingleConnection = !usePreference('enableMultipleConnectionSystem');
const itemPaddingStyles = useMemo(
() => getTreeItemStyles({ level, isExpandable: false }),
[level]
diff --git a/packages/compass-connections-navigation/src/sc-connections-navigation-tree.spec.tsx b/packages/compass-connections-navigation/src/sc-connections-navigation-tree.spec.tsx
index c6755e046df..663fb9a5640 100644
--- a/packages/compass-connections-navigation/src/sc-connections-navigation-tree.spec.tsx
+++ b/packages/compass-connections-navigation/src/sc-connections-navigation-tree.spec.tsx
@@ -67,7 +67,7 @@ const activeWorkspace = {
const dummyPreferences = {
getPreferences() {
return {
- enableNewMultipleConnectionSystem: false,
+ enableMultipleConnectionSystem: false,
};
},
onPreferenceValueChanged() {},
@@ -93,7 +93,8 @@ function renderComponent(
);
}
-describe('ConnectionsNavigationTree -- Single connection usage', function () {
+// TODO(COMPASS-7906): remove
+describe.skip('ConnectionsNavigationTree -- Single connection usage', function () {
let preferences: PreferencesAccess;
afterEach(cleanup);
@@ -106,7 +107,7 @@ describe('ConnectionsNavigationTree -- Single connection usage', function () {
beforeEach(async function () {
await preferences.savePreferences({
enableRenameCollectionModal: true,
- enableNewMultipleConnectionSystem: false,
+ enableMultipleConnectionSystem: false,
});
renderComponent(
diff --git a/packages/compass-connections-navigation/src/styled-navigation-item.tsx b/packages/compass-connections-navigation/src/styled-navigation-item.tsx
index 0693dde0619..a31c978bcca 100644
--- a/packages/compass-connections-navigation/src/styled-navigation-item.tsx
+++ b/packages/compass-connections-navigation/src/styled-navigation-item.tsx
@@ -25,9 +25,7 @@ export default function StyledNavigationItem({
const isDarkMode = useDarkMode();
const { connectionColorToHex, connectionColorToHexActive } =
useConnectionColor();
- const isSingleConnection = !usePreference(
- 'enableNewMultipleConnectionSystem'
- );
+ const isSingleConnection = !usePreference('enableMultipleConnectionSystem');
const { colorCode } = item;
const isDisconnectedConnection =
item.type === 'connection' &&
diff --git a/packages/compass-connections-navigation/src/tree-data.ts b/packages/compass-connections-navigation/src/tree-data.ts
index a485db6c0d5..320214beea7 100644
--- a/packages/compass-connections-navigation/src/tree-data.ts
+++ b/packages/compass-connections-navigation/src/tree-data.ts
@@ -18,9 +18,11 @@ type DatabaseOrCollectionStatus =
| 'error';
export type NotConnectedConnectionStatus =
- | ConnectionStatus.Connecting
- | ConnectionStatus.Disconnected
- | ConnectionStatus.Failed;
+ | 'initial'
+ | 'connecting'
+ | 'disconnected'
+ | 'canceled'
+ | 'failed';
export type NotConnectedConnection = {
name: string;
@@ -31,7 +33,7 @@ export type NotConnectedConnection = {
export type ConnectedConnection = {
name: string;
connectionInfo: ConnectionInfo;
- connectionStatus: ConnectionStatus.Connected;
+ connectionStatus: 'connected';
isReady: boolean;
isDataLake: boolean;
isWritable: boolean;
@@ -80,7 +82,7 @@ export type ConnectedConnectionTreeItem = VirtualTreeItem & {
colorCode?: string;
isExpanded: boolean;
connectionInfo: ConnectionInfo;
- connectionStatus: ConnectionStatus.Connected;
+ connectionStatus: 'connected';
isPerformanceTabSupported: boolean;
hasWriteActionsDisabled: boolean;
isShellEnabled: boolean;
diff --git a/packages/compass-connections-navigation/src/with-status-marker.tsx b/packages/compass-connections-navigation/src/with-status-marker.tsx
index 977344c0174..b10daf1c389 100644
--- a/packages/compass-connections-navigation/src/with-status-marker.tsx
+++ b/packages/compass-connections-navigation/src/with-status-marker.tsx
@@ -7,9 +7,11 @@ import {
import React from 'react';
export type StatusMarker =
+ | 'initial'
| 'connected'
| 'disconnected'
| 'connecting'
+ | 'canceled'
| 'failed';
export type StatusMarkerProps = {
status: StatusMarker;
@@ -92,6 +94,8 @@ const MARKER_COMPONENTS: Record = {
connecting: ConnectingStatusMarker,
failed: FailedStatusMarker,
disconnected: NoMarker,
+ initial: NoMarker,
+ canceled: NoMarker,
} as const;
const withStatusMarkerStyles = css({
diff --git a/packages/compass-connections/package.json b/packages/compass-connections/package.json
index 17f292a1f25..bf0b2bb1011 100644
--- a/packages/compass-connections/package.json
+++ b/packages/compass-connections/package.json
@@ -13,7 +13,7 @@
"email": "compass@mongodb.com"
},
"homepage": "https://github.com/mongodb-js/compass",
- "version": "1.38.0",
+ "version": "1.39.0",
"repository": {
"type": "git",
"url": "https://github.com/mongodb-js/compass.git"
@@ -25,10 +25,11 @@
],
"license": "SSPL",
"main": "dist/index.js",
- "compass:main": "src/index.ts",
+ "compass:main": "src/index.tsx",
"compass:exports": {
- ".": "./src/index.ts",
- "./provider": "./src/provider.ts"
+ ".": "./src/index.tsx",
+ "./provider": "./src/provider.ts",
+ "./test": "./src/test.tsx"
},
"types": "./dist/index.d.ts",
"scripts": {
@@ -51,31 +52,33 @@
"reformat": "npm run eslint . -- --fix && npm run prettier -- --write ."
},
"dependencies": {
- "@mongodb-js/compass-components": "^1.29.0",
- "@mongodb-js/compass-logging": "^1.4.3",
- "@mongodb-js/compass-maybe-protect-connection-string": "^0.24.0",
- "@mongodb-js/compass-telemetry": "^1.1.3",
- "@mongodb-js/compass-utils": "^0.6.9",
- "@mongodb-js/connection-form": "^1.36.0",
- "@mongodb-js/connection-info": "^0.5.3",
- "@mongodb-js/connection-storage": "^0.17.0",
+ "@mongodb-js/compass-components": "^1.29.1",
+ "@mongodb-js/compass-logging": "^1.4.4",
+ "@mongodb-js/compass-maybe-protect-connection-string": "^0.25.0",
+ "@mongodb-js/compass-telemetry": "^1.1.4",
+ "@mongodb-js/compass-utils": "^0.6.10",
+ "@mongodb-js/connection-form": "^1.37.0",
+ "@mongodb-js/connection-info": "^0.6.0",
+ "@mongodb-js/connection-storage": "^0.18.0",
"bson": "^6.7.0",
- "compass-preferences-model": "^2.26.0",
- "hadron-app-registry": "^9.2.2",
+ "compass-preferences-model": "^2.27.0",
+ "hadron-app-registry": "^9.2.3",
"lodash": "^4.17.21",
"mongodb-build-info": "^1.7.2",
"mongodb-connection-string-url": "^3.0.1",
- "mongodb-data-service": "^22.22.3",
- "react": "^17.0.2"
+ "mongodb-data-service": "^22.23.0",
+ "react": "^17.0.2",
+ "react-redux": "^8.1.3",
+ "redux": "^4.2.1",
+ "redux-thunk": "^2.4.2"
},
"devDependencies": {
- "@mongodb-js/eslint-config-compass": "^1.1.4",
- "@mongodb-js/mocha-config-compass": "^1.3.10",
+ "@mongodb-js/eslint-config-compass": "^1.1.5",
+ "@mongodb-js/mocha-config-compass": "^1.4.0",
"@mongodb-js/prettier-config-compass": "^1.0.2",
"@mongodb-js/tsconfig-compass": "^1.0.4",
"@testing-library/dom": "^8.20.1",
"@testing-library/react": "^12.1.5",
- "@testing-library/react-hooks": "^7.0.2",
"@testing-library/user-event": "^13.5.0",
"@types/chai": "^4.2.21",
"@types/chai-dom": "^0.0.10",
diff --git a/packages/compass-connections/src/components/connection-status-notifications.tsx b/packages/compass-connections/src/components/connection-status-notifications.tsx
index 9abeb860519..32d6cddea7a 100644
--- a/packages/compass-connections/src/components/connection-status-notifications.tsx
+++ b/packages/compass-connections/src/components/connection-status-notifications.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React from 'react';
import {
Body,
Code,
@@ -6,7 +6,8 @@ import {
Link,
showConfirmation,
spacing,
- useToast,
+ openToast,
+ closeToast,
} from '@mongodb-js/compass-components';
import type { ConnectionInfo } from '@mongodb-js/connection-info';
import { getConnectionTitle } from '@mongodb-js/connection-info';
@@ -90,158 +91,135 @@ const deviceAuthModalContentStyles = css({
},
});
-/**
- * Returns triggers for various notifications (toasts and modals) that are
- * supposed to be displayed every time connection flow is happening in the
- * application.
- *
- * All toasts and modals are only applicable in multiple connections mode. Right
- * now it's gated by the feature flag, the flag check can be removed when this
- * is the default behavior
- */
-export function useConnectionStatusNotifications() {
- const enableNewMultipleConnectionSystem = usePreference(
- 'enableNewMultipleConnectionSystem'
- );
- const { openToast, closeToast } = useToast('connection-status');
-
- const openConnectionStartedToast = useCallback(
- (connectionInfo: ConnectionInfo, onCancelClick: () => void) => {
- const { title, description } = getConnectingStatusText(connectionInfo);
- openToast(connectionInfo.id, {
- title,
- description,
- dismissible: true,
- variant: 'progress',
- actionElement: (
- {
- closeToast(connectionInfo.id);
- onCancelClick();
- }}
- data-testid="cancel-connection-button"
- >
- CANCEL
-
- ),
- });
- },
- [closeToast, openToast]
- );
+const openConnectionStartedToast = (
+ connectionInfo: ConnectionInfo,
+ onCancelClick: () => void
+) => {
+ const { title, description } = getConnectingStatusText(connectionInfo);
+ openToast(`connection-status--${connectionInfo.id}`, {
+ title,
+ description,
+ dismissible: true,
+ variant: 'progress',
+ actionElement: (
+ {
+ closeToast(`connection-status--${connectionInfo.id}`);
+ onCancelClick();
+ }}
+ data-testid="cancel-connection-button"
+ >
+ CANCEL
+
+ ),
+ });
+};
- const openConnectionSucceededToast = useCallback(
- (connectionInfo: ConnectionInfo) => {
- openToast(connectionInfo.id, {
- title: `Connected to ${getConnectionTitle(connectionInfo)}`,
- variant: 'success',
- timeout: 3_000,
- });
- },
- [openToast]
- );
+const openConnectionSucceededToast = (connectionInfo: ConnectionInfo) => {
+ openToast(`connection-status--${connectionInfo.id}`, {
+ title: `Connected to ${getConnectionTitle(connectionInfo)}`,
+ variant: 'success',
+ timeout: 3_000,
+ });
+};
- const openConnectionFailedToast = useCallback(
- (
- // Connection info might be missing if we failed connecting before we
- // could even resolve connection info. Currently the only case where this
- // can happen is autoconnect flow
- connectionInfo: ConnectionInfo | null | undefined,
- error: Error,
- onReviewClick: () => void
- ) => {
- const failedToastId = connectionInfo?.id ?? 'failed';
-
- openToast(failedToastId, {
- title: error.message,
- description: (
- {
- closeToast(failedToastId);
- onReviewClick();
- }}
- />
- ),
- variant: 'warning',
- });
- },
- [closeToast, openToast]
- );
+const openConnectionFailedToast = (
+ // Connection info might be missing if we failed connecting before we
+ // could even resolve connection info. Currently the only case where this
+ // can happen is autoconnect flow
+ connectionInfo: ConnectionInfo | null | undefined,
+ error: Error,
+ onReviewClick: () => void
+) => {
+ const failedToastId = connectionInfo?.id ?? 'failed';
+
+ openToast(`connection-status--${failedToastId}`, {
+ title: error.message,
+ description: (
+ {
+ closeToast(`connection-status--${failedToastId}`);
+ onReviewClick();
+ }}
+ />
+ ),
+ variant: 'warning',
+ });
+};
- const openMaximumConnectionsReachedToast = useCallback(
- (maxConcurrentConnections: number) => {
- const message = `Only ${maxConcurrentConnections} connection${
- maxConcurrentConnections > 1 ? 's' : ''
- } can be connected to at the same time. First disconnect from another connection.`;
-
- openToast('max-connections-reached', {
- title: 'Maximum concurrent connections limit reached',
- description: message,
- variant: 'warning',
- timeout: 5_000,
- });
- },
- [openToast]
- );
+const openMaximumConnectionsReachedToast = (
+ maxConcurrentConnections: number
+) => {
+ const message = `Only ${maxConcurrentConnections} connection${
+ maxConcurrentConnections > 1 ? 's' : ''
+ } can be connected to at the same time. First disconnect from another connection.`;
+
+ openToast('max-connections-reached', {
+ title: 'Maximum concurrent connections limit reached',
+ description: message,
+ variant: 'warning',
+ timeout: 5_000,
+ });
+};
- const openNotifyDeviceAuthModal = useCallback(
- (
- connectionInfo: ConnectionInfo,
- verificationUrl: string,
- userCode: string,
- onCancel: () => void,
- signal: AbortSignal
- ) => {
- void showConfirmation({
- title: `Complete authentication in the browser`,
- description: (
-
-
- Visit the following URL to complete authentication for{' '}
- {getConnectionTitle(connectionInfo)} :
-
-
-
- {verificationUrl}
-
-
-
- Enter the following code on that page:
-
-
- {userCode}
-
-
-
- ),
- hideConfirmButton: true,
- signal,
- }).then(
- (result) => {
- if (result === false) {
- onCancel?.();
- }
- },
- () => {
- // Abort signal was triggered
- }
- );
+const openNotifyDeviceAuthModal = (
+ connectionInfo: ConnectionInfo,
+ verificationUrl: string,
+ userCode: string,
+ onCancel: () => void,
+ signal: AbortSignal
+) => {
+ void showConfirmation({
+ title: `Complete authentication in the browser`,
+ description: (
+
+
+ Visit the following URL to complete authentication for{' '}
+ {getConnectionTitle(connectionInfo)} :
+
+
+
+ {verificationUrl}
+
+
+
+ Enter the following code on that page:
+
+
+ {userCode}
+
+
+
+ ),
+ hideConfirmButton: true,
+ signal,
+ }).then(
+ (result) => {
+ if (result === false) {
+ onCancel?.();
+ }
},
- []
+ () => {
+ // Abort signal was triggered
+ }
);
+};
- // Gated by the feature flag: if flag is on, we return trigger functions, if
- // flag is off, we return noop functions so that we can call them
- // unconditionally in the actual flow
- return enableNewMultipleConnectionSystem
+export function getNotificationTriggers(
+ enableMultipleConnectionSystem: boolean
+) {
+ return enableMultipleConnectionSystem
? {
openNotifyDeviceAuthModal,
openConnectionStartedToast,
openConnectionSucceededToast,
openConnectionFailedToast,
openMaximumConnectionsReachedToast,
- closeConnectionStatusToast: closeToast,
+ closeConnectionStatusToast: (connectionId: string) => {
+ return closeToast(`connection-status--${connectionId}`);
+ },
}
: {
openNotifyDeviceAuthModal: noop,
@@ -252,3 +230,23 @@ export function useConnectionStatusNotifications() {
closeConnectionStatusToast: noop,
};
}
+
+/**
+ * Returns triggers for various notifications (toasts and modals) that are
+ * supposed to be displayed every time connection flow is happening in the
+ * application.
+ *
+ * All toasts and modals are only applicable in multiple connections mode. Right
+ * now it's gated by the feature flag, the flag check can be removed when this
+ * is the default behavior
+ */
+export function useConnectionStatusNotifications() {
+ const enableMultipleConnectionSystem = usePreference(
+ 'enableMultipleConnectionSystem'
+ );
+
+ // Gated by the feature flag: if flag is on, we return trigger functions, if
+ // flag is off, we return noop functions so that we can call them
+ // unconditionally in the actual flow
+ return getNotificationTriggers(enableMultipleConnectionSystem);
+}
diff --git a/packages/compass-connections/src/components/connections-provider.tsx b/packages/compass-connections/src/components/connections-provider.tsx
deleted file mode 100644
index ffd3392d297..00000000000
--- a/packages/compass-connections/src/components/connections-provider.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-import React, { useContext, useEffect, useRef } from 'react';
-import { type ConnectionInfo, useConnectionsManagerContext } from '../provider';
-import { useConnections as useConnectionsStore } from '../stores/connections-store';
-import { useConnectionRepository as useConnectionsRepositoryState } from '../hooks/use-connection-repository';
-import { createServiceLocator } from 'hadron-app-registry';
-
-const ConnectionsStoreContext = React.createContext | null>(null);
-
-const ConnectionsRepositoryStateContext = React.createContext | null>(null);
-
-type UseConnectionsParams = Parameters[0];
-
-const ConnectionsStoreProvider: React.FunctionComponent<
- UseConnectionsParams
-> = ({ children, ...useConnectionsParams }) => {
- const connectionsStore = useConnectionsStore(useConnectionsParams);
- return (
-
- {children}
-
- );
-};
-
-export const ConnectionsProvider: React.FunctionComponent<
- UseConnectionsParams
-> = ({ children, ...useConnectionsParams }) => {
- const connectionsManagerRef = useRef(useConnectionsManagerContext());
- const connectionsRepositoryState = useConnectionsRepositoryState();
- useEffect(() => {
- const cm = connectionsManagerRef.current;
- return () => {
- void cm.closeAllConnections();
- };
- }, []);
- return (
-
-
- {children}
-
-
- );
-};
-
-export function useConnections() {
- const store = useContext(ConnectionsStoreContext);
- if (!store) {
- // TODO(COMPASS-7879): implement a default provider in test methods
- if (process.env.NODE_ENV === 'test') {
- // eslint-disable-next-line react-hooks/rules-of-hooks
- return useConnectionsStore();
- }
- throw new Error(
- 'Can not use useConnections outside of ConnectionsProvider component'
- );
- }
- return store;
-}
-
-export function useConnectionRepository() {
- const repository = useContext(ConnectionsRepositoryStateContext);
- if (!repository) {
- // TODO(COMPASS-7879): implement a default provider in test methods
- if (process.env.NODE_ENV === 'test') {
- // eslint-disable-next-line react-hooks/rules-of-hooks
- return useConnectionsRepositoryState();
- }
- throw new Error(
- 'Can not use useConnectionRepository outside of ConnectionsProvider component'
- );
- }
- return repository;
-}
-
-type FirstArgument = F extends (...args: [infer A, ...any]) => any
- ? A
- : F extends { new (...args: [infer A, ...any]): any }
- ? A
- : never;
-
-function withConnectionRepository<
- T extends ((...args: any[]) => any) | { new (...args: any[]): any }
->(
- ReactComponent: T
-): React.FunctionComponent, 'connectionRepository'>> {
- const WithConnectionRepository = (
- props: Omit, 'connectionRepository'> & React.Attributes
- ) => {
- const connectionRepository = useConnectionRepository();
- return React.createElement(ReactComponent, {
- ...props,
- connectionRepository,
- });
- };
- return WithConnectionRepository;
-}
-
-export { withConnectionRepository };
-
-export type ConnectionRepositoryAccess = Pick<
- ConnectionRepository,
- 'getConnectionInfoById'
->;
-
-export const useConnectionRepositoryAccess = (): ConnectionRepositoryAccess => {
- const repository = useConnectionRepository();
- const repositoryRef = useRef(repository);
- repositoryRef.current = repository;
- return {
- getConnectionInfoById(id: ConnectionInfo['id']) {
- return repositoryRef.current.getConnectionInfoById(id);
- },
- };
-};
-export const connectionRepositoryAccessLocator = createServiceLocator(
- useConnectionRepositoryAccess,
- 'connectionRepositoryAccessLocator'
-);
-
-export type ConnectionRepository = ReturnType;
-export { areConnectionsEqual } from '../hooks/use-connection-repository';
diff --git a/packages/compass-connections/src/components/legacy-connections.spec.tsx b/packages/compass-connections/src/components/legacy-connections.spec.tsx
index 5a4f5cd553c..ba498d552ca 100644
--- a/packages/compass-connections/src/components/legacy-connections.spec.tsx
+++ b/packages/compass-connections/src/components/legacy-connections.spec.tsx
@@ -1,92 +1,47 @@
import React from 'react';
-import {
- cleanup,
- render,
- screen,
- waitFor,
- fireEvent,
-} from '@testing-library/react';
import { expect } from 'chai';
-import type { ConnectionOptions, connect } from 'mongodb-data-service';
import { UUID } from 'bson';
import sinon from 'sinon';
import Connections from './legacy-connections';
-import { ToastArea } from '@mongodb-js/compass-components';
-import type { PreferencesAccess } from 'compass-preferences-model';
-import { createSandboxFromDefaultPreferences } from 'compass-preferences-model';
-import { PreferencesProvider } from 'compass-preferences-model/provider';
+import type { ConnectionInfo } from '../connection-info-provider';
import {
- InMemoryConnectionStorage,
- ConnectionStorageProvider,
- type ConnectionStorage,
- type ConnectionInfo,
-} from '@mongodb-js/connection-storage/provider';
-import { ConnectionsManager, ConnectionsManagerProvider } from '../provider';
-import type { DataService } from 'mongodb-data-service';
-import { createNoopLogger } from '@mongodb-js/compass-logging/provider';
-import { ConnectionsProvider } from './connections-provider';
-
-function getConnectionsManager(mockTestConnectFn?: typeof connect) {
- const { log } = createNoopLogger();
- return new ConnectionsManager({
- logger: log.unbound,
- __TEST_CONNECT_FN: mockTestConnectFn,
- });
-}
+ renderWithConnections,
+ screen,
+ userEvent,
+ waitFor,
+ cleanup,
+} from '../test';
async function loadSavedConnectionAndConnect(connectionInfo: ConnectionInfo) {
const savedConnectionButton = screen.getByTestId(
`saved-connection-button-${connectionInfo.id}`
);
- fireEvent.click(savedConnectionButton);
+ userEvent.click(savedConnectionButton);
// Wait for the connection to load in the form.
await waitFor(() =>
- expect(screen.queryByRole('textbox')?.textContent).to.equal(
+ expect(screen.queryByTestId('connectionString')?.textContent).to.equal(
connectionInfo.connectionOptions.connectionString
)
);
- const connectButton = screen.getByText('Connect');
- fireEvent.click(connectButton);
+ const connectButton = screen.getByRole('button', { name: 'Connect' });
+ userEvent.click(connectButton);
// Wait for the connecting... modal to hide.
await waitFor(() => expect(screen.queryByText('Cancel')).to.not.exist);
}
-describe('Connections Component', function () {
- let preferences: PreferencesAccess;
-
- before(async function () {
- preferences = await createSandboxFromDefaultPreferences();
- await preferences.savePreferences({ persistOIDCTokens: false });
- });
-
+// TODO(COMPASS-7906): remove
+describe.skip('Connections Component', function () {
afterEach(function () {
sinon.restore();
cleanup();
});
context('when rendered', function () {
- let loadConnectionsSpy: sinon.SinonSpy;
beforeEach(function () {
- const mockStorage = new InMemoryConnectionStorage([]);
- loadConnectionsSpy = sinon.spy(mockStorage, 'loadAll');
- render(
-
-
-
-
-
-
-
-
-
- );
- });
-
- it('calls once to load the connections', function () {
- expect(loadConnectionsSpy.callCount).to.equal(1);
+ renderWithConnections( );
});
it('renders the connect button from the connect-form', function () {
@@ -128,17 +83,16 @@ describe('Connections Component', function () {
});
context('when rendered with saved connections in storage', function () {
- let connectSpyFn: sinon.SinonSpy;
- let mockStorage: ConnectionStorage;
let savedConnectionId: string;
let savedConnectionWithAppNameId: string;
- let saveConnectionSpy: sinon.SinonSpy;
let connections: ConnectionInfo[];
+ let connectSpyFn: sinon.SinonSpy;
+ let saveConnectionSpy: sinon.SinonSpy;
+ let getState;
beforeEach(async function () {
savedConnectionId = new UUID().toString();
savedConnectionWithAppNameId = new UUID().toString();
- saveConnectionSpy = sinon.spy();
connections = [
{
@@ -156,30 +110,23 @@ describe('Connections Component', function () {
},
},
];
- mockStorage = new InMemoryConnectionStorage(connections);
- sinon.replace(mockStorage, 'save', saveConnectionSpy);
-
- const connectionsManager = getConnectionsManager(() => {
- return Promise.resolve({
- mockDataService: 'yes',
- addReauthenticationHandler() {},
- } as unknown as DataService);
- });
- connectSpyFn = sinon.spy(connectionsManager, 'connect');
-
- render(
-
-
-
-
-
-
-
-
-
+
+ connectSpyFn = sinon.stub().returns({});
+
+ const { connectionsStore, connectionStorage } = renderWithConnections(
+ ,
+ {
+ connections,
+ connectFn: connectSpyFn,
+ }
);
- await waitFor(() => expect(screen.queryAllByRole('listitem')).to.exist);
+ saveConnectionSpy = sinon.spy(connectionStorage, 'save');
+ getState = connectionsStore.getState;
+
+ await waitFor(() => {
+ expect(screen.queryAllByRole('listitem')).to.exist;
+ });
});
it('should render the saved connections', function () {
@@ -200,54 +147,28 @@ describe('Connections Component', function () {
context(
'when a saved connection is clicked on and connected to',
function () {
- const _Date = globalThis.Date;
beforeEach(async function () {
- globalThis.Date = class {
- constructor() {
- return new _Date(0);
- }
- static now() {
- return 0;
- }
- } as DateConstructor;
await loadSavedConnectionAndConnect(
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
connections.find(({ id }) => id === savedConnectionId)!
);
});
- afterEach(function () {
- globalThis.Date = _Date;
- });
-
- it('should call the connect function on ConnectionsManager with the connection options to connect', function () {
+ it('should call the connect function with the connection options to connect', function () {
expect(connectSpyFn.callCount).to.equal(1);
- expect(
- connectSpyFn.firstCall.args[0].connectionOptions
- ).to.deep.equal({
- connectionString:
- 'mongodb://localhost:27018/?readPreference=primary&ssl=false',
- });
+ expect(connectSpyFn.firstCall.args[0]).to.have.property(
+ 'connectionString',
+ 'mongodb://localhost:27018/?readPreference=primary&ssl=false&appName=TEST'
+ );
});
- it('should call to save the connection with the connection config', function () {
+ it('should call to save the connection', function () {
expect(saveConnectionSpy.callCount).to.equal(1);
- expect(
- saveConnectionSpy.firstCall.args[0].connectionInfo.id
- ).to.equal(savedConnectionId);
- expect(
- saveConnectionSpy.firstCall.args[0].connectionInfo.connectionOptions
- ).to.deep.equal({
- connectionString:
- 'mongodb://localhost:27018/?readPreference=primary&ssl=false',
- });
});
- it('should call to save the connection with a new lastUsed time', function () {
- expect(saveConnectionSpy.callCount).to.equal(1);
+ it('should update the connection with a new lastUsed time', function () {
expect(
- saveConnectionSpy.firstCall.args[0].connectionInfo.lastUsed.getTime()
- ).to.equal(0);
+ getState().connections.byId[savedConnectionId].info
+ ).to.have.property('lastUsed');
});
}
);
@@ -264,12 +185,10 @@ describe('Connections Component', function () {
it('should call the connect function without replacing appName', function () {
expect(connectSpyFn.callCount).to.equal(1);
- expect(
- connectSpyFn.firstCall.args[0].connectionOptions
- ).to.deep.equal({
- connectionString:
- 'mongodb://localhost:27019/?appName=Some+App+Name',
- });
+ expect(connectSpyFn.firstCall.args[0]).to.have.property(
+ 'connectionString',
+ 'mongodb://localhost:27019/?appName=Some+App+Name'
+ );
});
}
);
@@ -278,48 +197,15 @@ describe('Connections Component', function () {
context(
'when connecting to a connection that is not succeeding',
function () {
- let mockConnectFn: sinon.SinonSpy;
- let saveConnectionSpy: sinon.SinonSpy;
let savedConnectableId: string;
let savedUnconnectableId: string;
let connections: ConnectionInfo[];
let connectSpyFn: sinon.SinonSpy;
+ let saveConnectionSpy: sinon.SinonSpy;
beforeEach(async function () {
- saveConnectionSpy = sinon.spy();
savedConnectableId = new UUID().toString();
savedUnconnectableId = new UUID().toString();
-
- mockConnectFn = sinon.fake(
- async ({
- connectionOptions,
- }: {
- connectionOptions: ConnectionOptions;
- }) => {
- if (
- connectionOptions.connectionString ===
- 'mongodb://localhost:27099/?connectTimeoutMS=5000&serverSelectionTimeoutMS=5000'
- ) {
- return new Promise((resolve) => {
- // On first call we want this attempt to be cancelled before
- // this promise resolves.
- setTimeout(() => {
- resolve({
- mockDataService: 'yes',
- addReauthenticationHandler() {},
- });
- }, 500);
- });
- }
- return Promise.resolve({
- mockDataService: 'yes',
- addReauthenticationHandler() {},
- });
- }
- );
-
- const connectionsManager = getConnectionsManager(mockConnectFn);
- connectSpyFn = sinon.spy(connectionsManager, 'connect');
connections = [
{
id: savedConnectableId,
@@ -336,21 +222,28 @@ describe('Connections Component', function () {
},
},
];
- const mockStorage = new InMemoryConnectionStorage(connections);
- sinon.replace(mockStorage, 'save', saveConnectionSpy);
-
- render(
-
-
-
-
-
-
-
-
-
+
+ connectSpyFn = sinon
+ .stub()
+ // On first call we cancel it, so just never resolve to give UI time
+ // to render the connecting... state
+ .onFirstCall()
+ .callsFake(() => {
+ return new Promise(() => {});
+ })
+ // On second call connect successfully without blocking
+ .onSecondCall()
+ .callsFake(() => {
+ return {};
+ });
+
+ const { connectionStorage } = renderWithConnections(
+ ,
+ { connections, connectFn: connectSpyFn }
);
+ saveConnectionSpy = sinon.spy(connectionStorage, 'save');
+
await waitFor(
() =>
expect(
@@ -363,31 +256,35 @@ describe('Connections Component', function () {
const savedConnectionButton = screen.getByTestId(
`saved-connection-button-${savedUnconnectableId}`
);
- fireEvent.click(savedConnectionButton);
+ userEvent.click(savedConnectionButton);
// Wait for the connection to load in the form.
await waitFor(() =>
- expect(screen.queryByRole('textbox')?.textContent).to.equal(
+ expect(
+ screen.queryByTestId('connectionString')?.textContent
+ ).to.equal(
'mongodb://localhost:27099/?connectTimeoutMS=5000&serverSelectionTimeoutMS=5000'
)
);
- const connectButton = screen.getByText('Connect');
- fireEvent.click(connectButton);
+ const connectButton = screen.getByRole('button', { name: 'Connect' });
+ userEvent.click(connectButton);
// Wait for the connecting... modal to be shown.
- await waitFor(() => expect(screen.queryByText('Cancel')).to.be.visible);
+ await waitFor(() => {
+ expect(screen.queryByText('Cancel')).to.be.visible;
+ });
});
context('when the connection attempt is cancelled', function () {
beforeEach(async function () {
- const cancelButton = screen.getByText('Cancel');
- fireEvent.click(cancelButton);
+ const cancelButton = screen.getByRole('button', { name: 'Cancel' });
+ userEvent.click(cancelButton);
// Wait for the connecting... modal to hide.
- await waitFor(
- () => expect(screen.queryByText('Cancel')).to.not.exist
- );
+ await waitFor(() => {
+ expect(screen.queryByText('Cancel')).to.not.exist;
+ });
});
it('should enable the connect button', function () {
@@ -405,12 +302,10 @@ describe('Connections Component', function () {
it('should call the connect function with the connection options to connect', function () {
expect(connectSpyFn.callCount).to.equal(1);
- expect(
- connectSpyFn.firstCall.args[0].connectionOptions
- ).to.deep.equal({
- connectionString:
- 'mongodb://localhost:27099/?connectTimeoutMS=5000&serverSelectionTimeoutMS=5000',
- });
+ expect(connectSpyFn.firstCall.args[0]).to.have.property(
+ 'connectionString',
+ 'mongodb://localhost:27099/?connectTimeoutMS=5000&serverSelectionTimeoutMS=5000&appName=TEST'
+ );
});
context(
@@ -429,12 +324,10 @@ describe('Connections Component', function () {
it('should call the connect function with the connection options to connect', function () {
expect(connectSpyFn.callCount).to.equal(2);
- expect(
- connectSpyFn.secondCall.args[0].connectionOptions
- ).to.deep.equal({
- connectionString:
- 'mongodb://localhost:27018/?readPreference=primary&ssl=false',
- });
+ expect(connectSpyFn.secondCall.args[0]).to.have.property(
+ 'connectionString',
+ 'mongodb://localhost:27018/?readPreference=primary&ssl=false&appName=TEST'
+ );
});
}
);
diff --git a/packages/compass-connections/src/components/legacy-connections.tsx b/packages/compass-connections/src/components/legacy-connections.tsx
index 5538a043c52..b47138d32bb 100644
--- a/packages/compass-connections/src/components/legacy-connections.tsx
+++ b/packages/compass-connections/src/components/legacy-connections.tsx
@@ -11,30 +11,21 @@ import {
import { useLogger } from '@mongodb-js/compass-logging/provider';
import ConnectionForm from '@mongodb-js/connection-form';
import type AppRegistry from 'hadron-app-registry';
-import type { connect } from 'mongodb-data-service';
-import React, { useEffect, useState } from 'react';
+import React, { useCallback } from 'react';
import { usePreference } from 'compass-preferences-model/provider';
import type { ConnectionInfo } from '../provider';
-import {
- ConnectionStatus,
- useConnectionRepository,
- useConnections,
-} from '../provider';
+import { useConnectionRepository } from '../hooks/use-connection-repository';
import Connecting from './connecting/connecting';
import ConnectionList from './connection-list/connection-list';
import FormHelp from './form-help/form-help';
import { useConnectionInfoStatus } from '../hooks/use-connections-with-status';
-import { createNewConnectionInfo } from '../stores/connections-store';
+import { useConnections } from '../stores/connections-store';
import {
getConnectingStatusText,
getConnectionErrorMessage,
} from './connection-status-notifications';
import { useConnectionFormPreferences } from '../hooks/use-connection-form-preferences';
-type ConnectFn = typeof connect;
-
-export type { ConnectFn };
-
const connectStyles = css({
position: 'absolute',
left: 0,
@@ -86,23 +77,6 @@ const formCardLightThemeStyles = css({
background: palette.white,
});
-// Single connection form is a bit of a special case where form is always on
-// screen so even when user is not explicitly editing any connections, we need a
-// connection info object to be passed around. For that purposes this hook will
-// either return an editing connection info or will create a new one as a
-// fallback if nothing is actively being edited
-function useActiveConnectionInfo(
- editingConnectionInfo?: ConnectionInfo | null
-) {
- const [connectionInfo, setConnectionInfo] = useState(() => {
- return editingConnectionInfo ?? createNewConnectionInfo();
- });
- useEffect(() => {
- setConnectionInfo(editingConnectionInfo ?? createNewConnectionInfo());
- }, [editingConnectionInfo]);
- return connectionInfo;
-}
-
function Connections({
appRegistry,
openConnectionImportExportModal,
@@ -115,7 +89,11 @@ function Connections({
const { log, mongoLogId } = useLogger('COMPASS-CONNECTIONS');
const {
- state: { editingConnectionInfo, connectionErrors, oidcDeviceAuthState },
+ state: {
+ editingConnectionInfo: activeConnectionInfo,
+ connectionErrors,
+ oidcDeviceAuthState,
+ },
connect,
disconnect,
createNewConnection,
@@ -126,31 +104,17 @@ function Connections({
saveEditedConnection,
} = useConnections();
+ const activeConnectionStatus = useConnectionInfoStatus(
+ activeConnectionInfo.id
+ );
+
const { favoriteConnections, nonFavoriteConnections: recentConnections } =
useConnectionRepository();
const darkMode = useDarkMode();
const connectionFormPreferences = useConnectionFormPreferences();
const isMultiConnectionEnabled = usePreference(
- 'enableNewMultipleConnectionSystem'
- );
-
- const activeConnectionInfo = useActiveConnectionInfo(
- // TODO(COMPASS-7397): Even though connection form interface expects
- // connection info to only be "initial", some parts of the form UI actually
- // read the values from the info as if they should be updated (favorite edit
- // form), for that purpose instead of using state store directly, we will
- // first try to find the connection in the list of connections that track
- // the connection info updates instead of passing the store state directly.
- // This should go away when we are normalizing this state and making sure
- // that favorite form is correctly reading the state from a single store
- [...favoriteConnections, ...recentConnections].find((info) => {
- // Might be missing in case of "New connection" when it's not saved yet
- return info.id === editingConnectionInfo?.id;
- }) ?? editingConnectionInfo
- );
- const activeConnectionStatus = useConnectionInfoStatus(
- activeConnectionInfo.id
+ 'enableMultipleConnectionSystem'
);
const onConnectClick = (connectionInfo: ConnectionInfo) => {
@@ -169,6 +133,11 @@ function Connections({
const activeConnectionOidcAuthState =
oidcDeviceAuthState[activeConnectionInfo.id];
+ const openSettingsModal = useCallback(
+ (tab?: string) => appRegistry.emit('open-compass-settings', tab),
+ [appRegistry]
+ );
+
return (
@@ -223,6 +192,7 @@ function Connections({
initialConnectionInfo={activeConnectionInfo}
connectionErrorMessage={connectionErrorMessage}
preferences={connectionFormPreferences}
+ openSettingsModal={openSettingsModal}
/>
@@ -230,7 +200,7 @@ function Connections({
- {activeConnectionStatus === ConnectionStatus.Connecting && (
+ {activeConnectionStatus === 'connecting' && (