From a2981325988865c3addefd53f8bffb08d0d5531b Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Thu, 4 Sep 2025 08:46:33 -0500 Subject: [PATCH 01/11] WIP --- .../collection-header-actions.spec.tsx | 51 +++++++++++++++++++ .../collection-header-actions.tsx | 4 +- .../collection-header/collection-header.tsx | 18 ++++++- 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx b/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx index 54f18833a16..3afdfaedb99 100644 --- a/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx +++ b/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx @@ -57,6 +57,7 @@ describe('CollectionHeaderActions [Component]', function () { namespace="test.test" isReadonly={false} onOpenMockDataModal={sinon.stub()} + hasData={true} {...props} /> @@ -374,5 +375,55 @@ describe('CollectionHeaderActions [Component]', function () { expect(onOpenMockDataModal).to.have.been.calledOnce; }); + + it('should disable Mock Data Generator button when hasData is false', async function () { + mockUseAssignment.returns({ + assignment: { + assignmentData: { + variant: 'mockDataGeneratorVariant', + }, + }, + }); + + await renderCollectionHeaderActions( + { + namespace: 'test.collection', + isReadonly: false, + hasData: false, + }, + {}, + atlasConnectionInfo + ); + + const button = screen.getByTestId( + 'collection-header-generate-mock-data-button' + ); + expect(button).to.have.attribute('aria-disabled', 'true'); + }); + + it('should enable Mock Data Generator button when hasData is true', async function () { + mockUseAssignment.returns({ + assignment: { + assignmentData: { + variant: 'mockDataGeneratorVariant', + }, + }, + }); + + await renderCollectionHeaderActions( + { + namespace: 'test.collection', + isReadonly: false, + hasData: true, + }, + {}, + atlasConnectionInfo + ); + + const button = screen.getByTestId( + 'collection-header-generate-mock-data-button' + ); + expect(button).to.not.have.attribute('aria-disabled', 'true'); + }); }); }); 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 e6f06f6525e..e46d97bc857 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 @@ -47,6 +47,7 @@ type CollectionHeaderActionsProps = { sourceName?: string; sourcePipeline?: unknown[]; onOpenMockDataModal: () => void; + hasData: boolean; }; const CollectionHeaderActions: React.FunctionComponent< @@ -58,6 +59,7 @@ const CollectionHeaderActions: React.FunctionComponent< sourceName, sourcePipeline, onOpenMockDataModal, + hasData, }: CollectionHeaderActionsProps) => { const connectionInfo = useConnectionInfo(); const { id: connectionId, atlasMetadata } = connectionInfo; @@ -87,8 +89,6 @@ const CollectionHeaderActions: React.FunctionComponent< !sourceName; // sourceName indicates it's a view // TODO: CLOUDP-337090: also filter out overly nested collections - const hasData = true; // TODO: CLOUDP-337090 - return (
void; + hasData: boolean; }; const getInsightsForPipeline = (pipeline: any[], isAtlas: boolean) => { @@ -97,6 +100,7 @@ const CollectionHeader: React.FunctionComponent = ({ editViewName, sourcePipeline, onOpenMockDataModal, + hasData, }) => { const darkMode = useDarkMode(); const showInsights = usePreference('showInsights'); @@ -174,6 +178,7 @@ const CollectionHeader: React.FunctionComponent = ({ sourceName={sourceName} sourcePipeline={sourcePipeline} onOpenMockDataModal={onOpenMockDataModal} + hasData={hasData} />
@@ -181,7 +186,18 @@ const CollectionHeader: React.FunctionComponent = ({ ); }; -const ConnectedCollectionHeader = connect(undefined, { +const mapStateToProps = (state: CollectionState) => { + const { schemaAnalysis } = state; + + return { + hasData: + schemaAnalysis.status === SCHEMA_ANALYSIS_STATE_COMPLETE && + schemaAnalysis.processedSchema && + Object.keys(schemaAnalysis.processedSchema).length > 0, + }; +}; + +const ConnectedCollectionHeader = connect(mapStateToProps, { onOpenMockDataModal: openMockDataGeneratorModal, })(CollectionHeader); From a3052fca76ba8753783bbc0e5c231db805194d1e Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Thu, 4 Sep 2025 09:29:36 -0500 Subject: [PATCH 02/11] WIP --- .../collection-header-actions.spec.tsx | 26 +++++++++++++++++++ .../collection-header-actions.tsx | 6 +++-- .../collection-header/collection-header.tsx | 8 ++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx b/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx index 3afdfaedb99..b34cbd6d66f 100644 --- a/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx +++ b/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx @@ -58,6 +58,7 @@ describe('CollectionHeaderActions [Component]', function () { isReadonly={false} onOpenMockDataModal={sinon.stub()} hasData={true} + maxNestingDepth={2} {...props} /> @@ -425,5 +426,30 @@ describe('CollectionHeaderActions [Component]', function () { ); expect(button).to.not.have.attribute('aria-disabled', 'true'); }); + + it('should not show Mock Data Generator button for collections with excessive nesting depth', async function () { + mockUseAssignment.returns({ + assignment: { + assignmentData: { + variant: 'mockDataGeneratorVariant', + }, + }, + }); + + await renderCollectionHeaderActions( + { + namespace: 'test.collection', + isReadonly: false, + hasData: true, + maxNestingDepth: 4, // Exceeds the limit of 3 + }, + {}, + atlasConnectionInfo + ); + + expect( + screen.queryByTestId('collection-header-generate-mock-data-button') + ).to.not.exist; + }); }); }); 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 e46d97bc857..b244cefedb6 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 @@ -48,6 +48,7 @@ type CollectionHeaderActionsProps = { sourcePipeline?: unknown[]; onOpenMockDataModal: () => void; hasData: boolean; + maxNestingDepth: number; }; const CollectionHeaderActions: React.FunctionComponent< @@ -60,6 +61,7 @@ const CollectionHeaderActions: React.FunctionComponent< sourcePipeline, onOpenMockDataModal, hasData, + maxNestingDepth, }: CollectionHeaderActionsProps) => { const connectionInfo = useConnectionInfo(); const { id: connectionId, atlasMetadata } = connectionInfo; @@ -86,8 +88,8 @@ const CollectionHeaderActions: React.FunctionComponent< isInMockDataTreatmentVariant && atlasMetadata && // Only show in Atlas !isReadonly && // Don't show for readonly collections (views) - !sourceName; // sourceName indicates it's a view - // TODO: CLOUDP-337090: also filter out overly nested collections + !sourceName && // sourceName indicates it's a view + maxNestingDepth < 4; // Filter out overly nested collections (4+ levels) return (
void; hasData: boolean; + maxNestingDepth: number; }; const getInsightsForPipeline = (pipeline: any[], isAtlas: boolean) => { @@ -101,6 +102,7 @@ const CollectionHeader: React.FunctionComponent = ({ sourcePipeline, onOpenMockDataModal, hasData, + maxNestingDepth, }) => { const darkMode = useDarkMode(); const showInsights = usePreference('showInsights'); @@ -179,6 +181,7 @@ const CollectionHeader: React.FunctionComponent = ({ sourcePipeline={sourcePipeline} onOpenMockDataModal={onOpenMockDataModal} hasData={hasData} + maxNestingDepth={maxNestingDepth} />
@@ -194,6 +197,11 @@ const mapStateToProps = (state: CollectionState) => { schemaAnalysis.status === SCHEMA_ANALYSIS_STATE_COMPLETE && schemaAnalysis.processedSchema && Object.keys(schemaAnalysis.processedSchema).length > 0, + maxNestingDepth: + schemaAnalysis.status === SCHEMA_ANALYSIS_STATE_COMPLETE && + schemaAnalysis.schemaMetadata + ? schemaAnalysis.schemaMetadata.maxNestingDepth + : 0, }; }; From 4050da7162bf9c9d0e373450f9c6feafee999637 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Thu, 4 Sep 2025 10:20:43 -0500 Subject: [PATCH 03/11] Handle undefined --- .../collection-header.spec.tsx | 31 +++++++++++++++++++ .../collection-header/collection-header.tsx | 2 ++ 2 files changed, 33 insertions(+) diff --git a/packages/compass-collection/src/components/collection-header/collection-header.spec.tsx b/packages/compass-collection/src/components/collection-header/collection-header.spec.tsx index 0a7b3118ce8..dba3a7760a3 100644 --- a/packages/compass-collection/src/components/collection-header/collection-header.spec.tsx +++ b/packages/compass-collection/src/components/collection-header/collection-header.spec.tsx @@ -294,4 +294,35 @@ describe('CollectionHeader [Component]', function () { }); }); }); + + it('should handle undefined schemaAnalysis gracefully and render collection header successfully', function () { + // Create a store with undefined schemaAnalysis to simulate initial state + const mockStoreWithUndefinedSchema = createStore(() => ({ + mockDataGenerator: { + isModalOpen: false, + currentStep: MockDataGeneratorStep.SCHEMA_CONFIRMATION, + }, + // schemaAnalysis not provided + })); + + expect(() => { + renderWithConnections( + + + + + + ); + }).to.not.throw(); + + expect(screen.getByTestId('collection-header')).to.exist; + expect(screen.getByTestId('collection-header-actions')).to.exist; + }); }); diff --git a/packages/compass-collection/src/components/collection-header/collection-header.tsx b/packages/compass-collection/src/components/collection-header/collection-header.tsx index 5f2bba7a02c..3bbda649d5a 100644 --- a/packages/compass-collection/src/components/collection-header/collection-header.tsx +++ b/packages/compass-collection/src/components/collection-header/collection-header.tsx @@ -194,10 +194,12 @@ const mapStateToProps = (state: CollectionState) => { return { hasData: + schemaAnalysis && schemaAnalysis.status === SCHEMA_ANALYSIS_STATE_COMPLETE && schemaAnalysis.processedSchema && Object.keys(schemaAnalysis.processedSchema).length > 0, maxNestingDepth: + schemaAnalysis && schemaAnalysis.status === SCHEMA_ANALYSIS_STATE_COMPLETE && schemaAnalysis.schemaMetadata ? schemaAnalysis.schemaMetadata.maxNestingDepth From 5a4fbfcb497a21c2e41b161fc2a365c29806bd58 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Thu, 4 Sep 2025 10:35:53 -0500 Subject: [PATCH 04/11] Comments --- .../collection-header-actions.spec.tsx | 16 +++++++------- .../collection-header-actions.tsx | 19 ++++++++++------- .../collection-header/collection-header.tsx | 21 ++++++++----------- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx b/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx index b34cbd6d66f..763dfa1bbeb 100644 --- a/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx +++ b/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx @@ -57,8 +57,8 @@ describe('CollectionHeaderActions [Component]', function () { namespace="test.test" isReadonly={false} onOpenMockDataModal={sinon.stub()} - hasData={true} - maxNestingDepth={2} + hasSchemaAnalysisData={true} + analyzedSchemaDepth={2} {...props} /> @@ -377,7 +377,7 @@ describe('CollectionHeaderActions [Component]', function () { expect(onOpenMockDataModal).to.have.been.calledOnce; }); - it('should disable Mock Data Generator button when hasData is false', async function () { + it('should disable Mock Data Generator button when hasSchemaAnalysisData is false', async function () { mockUseAssignment.returns({ assignment: { assignmentData: { @@ -390,7 +390,7 @@ describe('CollectionHeaderActions [Component]', function () { { namespace: 'test.collection', isReadonly: false, - hasData: false, + hasSchemaAnalysisData: false, }, {}, atlasConnectionInfo @@ -402,7 +402,7 @@ describe('CollectionHeaderActions [Component]', function () { expect(button).to.have.attribute('aria-disabled', 'true'); }); - it('should enable Mock Data Generator button when hasData is true', async function () { + it('should enable Mock Data Generator button when hasSchemaAnalysisData is true', async function () { mockUseAssignment.returns({ assignment: { assignmentData: { @@ -415,7 +415,7 @@ describe('CollectionHeaderActions [Component]', function () { { namespace: 'test.collection', isReadonly: false, - hasData: true, + hasSchemaAnalysisData: true, }, {}, atlasConnectionInfo @@ -440,8 +440,8 @@ describe('CollectionHeaderActions [Component]', function () { { namespace: 'test.collection', isReadonly: false, - hasData: true, - maxNestingDepth: 4, // Exceeds the limit of 3 + hasSchemaAnalysisData: true, + analyzedSchemaDepth: 4, // Exceeds the limit of 3 }, {}, atlasConnectionInfo 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 b244cefedb6..77a5b7efb8f 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 @@ -19,6 +19,11 @@ import { ExperimentTestGroup, } from '@mongodb-js/compass-telemetry/provider'; +/** + * Maximum allowed nesting depth for collections to show Mock Data Generator + */ +const MAX_COLLECTION_NESTING_DEPTH = 4; + const collectionHeaderActionsStyles = css({ display: 'flex', alignItems: 'center', @@ -47,8 +52,8 @@ type CollectionHeaderActionsProps = { sourceName?: string; sourcePipeline?: unknown[]; onOpenMockDataModal: () => void; - hasData: boolean; - maxNestingDepth: number; + hasSchemaAnalysisData: boolean; + analyzedSchemaDepth: number; }; const CollectionHeaderActions: React.FunctionComponent< @@ -60,8 +65,8 @@ const CollectionHeaderActions: React.FunctionComponent< sourceName, sourcePipeline, onOpenMockDataModal, - hasData, - maxNestingDepth, + hasSchemaAnalysisData, + analyzedSchemaDepth, }: CollectionHeaderActionsProps) => { const connectionInfo = useConnectionInfo(); const { id: connectionId, atlasMetadata } = connectionInfo; @@ -89,7 +94,7 @@ const CollectionHeaderActions: React.FunctionComponent< atlasMetadata && // Only show in Atlas !isReadonly && // Don't show for readonly collections (views) !sourceName && // sourceName indicates it's a view - maxNestingDepth < 4; // Filter out overly nested collections (4+ levels) + analyzedSchemaDepth < MAX_COLLECTION_NESTING_DEPTH; // Filter out overly nested collections return (
@@ -193,15 +193,12 @@ const mapStateToProps = (state: CollectionState) => { const { schemaAnalysis } = state; return { - hasData: + hasSchemaAnalysisData: schemaAnalysis && schemaAnalysis.status === SCHEMA_ANALYSIS_STATE_COMPLETE && - schemaAnalysis.processedSchema && Object.keys(schemaAnalysis.processedSchema).length > 0, - maxNestingDepth: - schemaAnalysis && - schemaAnalysis.status === SCHEMA_ANALYSIS_STATE_COMPLETE && - schemaAnalysis.schemaMetadata + analyzedSchemaDepth: + schemaAnalysis && schemaAnalysis.status === SCHEMA_ANALYSIS_STATE_COMPLETE ? schemaAnalysis.schemaMetadata.maxNestingDepth : 0, }; From a133e0a92665373ec6cac2f65c78118043f146b1 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Thu, 4 Sep 2025 11:16:17 -0500 Subject: [PATCH 05/11] Move tests --- .../collection-header-actions.spec.tsx | 188 ------------ .../collection-header.spec.tsx | 269 +++++++++++++++++- 2 files changed, 266 insertions(+), 191 deletions(-) diff --git a/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx b/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx index 763dfa1bbeb..f10bd1646fd 100644 --- a/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx +++ b/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx @@ -223,119 +223,6 @@ describe('CollectionHeaderActions [Component]', function () { }, }; - it('should not show Mock Data Generator button when user is in control group', async function () { - mockUseAssignment.returns({ - assignment: { - assignmentData: { - variant: 'mockDataGeneratorControl', - }, - }, - }); - - await renderCollectionHeaderActions( - { - namespace: 'test.collection', - isReadonly: false, - }, - {}, - atlasConnectionInfo - ); - - expect( - screen.queryByTestId('collection-header-generate-mock-data-button') - ).to.not.exist; - }); - - it('should not show Mock Data Generator button when not in Atlas', async function () { - mockUseAssignment.returns({ - assignment: { - assignmentData: { - variant: 'treatment', - }, - }, - }); - - await renderCollectionHeaderActions({ - namespace: 'test.collection', - isReadonly: false, - // Don't pass atlasConnectionInfo, to simulate not being in Atlas - }); - - expect( - screen.queryByTestId('collection-header-generate-mock-data-button') - ).to.not.exist; - }); - - it('should not show Mock Data Generator button for readonly collections', async function () { - mockUseAssignment.returns({ - assignment: { - assignmentData: { - variant: 'treatment', - }, - }, - }); - - await renderCollectionHeaderActions( - { - namespace: 'test.collection', - isReadonly: true, - }, - {}, - atlasConnectionInfo - ); - - expect( - screen.queryByTestId('collection-header-generate-mock-data-button') - ).to.not.exist; - }); - - it('should not show Mock Data Generator button for views (sourceName present)', async function () { - mockUseAssignment.returns({ - assignment: { - assignmentData: { - variant: 'treatment', - }, - }, - }); - - await renderCollectionHeaderActions( - { - namespace: 'test.collection', - isReadonly: false, - sourceName: 'source-collection', - }, - {}, - atlasConnectionInfo - ); - - expect( - screen.queryByTestId('collection-header-generate-mock-data-button') - ).to.not.exist; - }); - - it('should show Mock Data Generator button when user is in treatment group and in Atlas', async function () { - mockUseAssignment.returns({ - assignment: { - assignmentData: { - variant: 'mockDataGeneratorVariant', - }, - }, - }); - - await renderCollectionHeaderActions( - { - namespace: 'test.collection', - isReadonly: false, - }, - {}, - atlasConnectionInfo - ); - - expect( - screen.getByTestId('collection-header-generate-mock-data-button') - ).to.exist; - }); - it('should call useAssignment with correct parameters', async function () { await renderCollectionHeaderActions({ namespace: 'test.collection', @@ -376,80 +263,5 @@ describe('CollectionHeaderActions [Component]', function () { expect(onOpenMockDataModal).to.have.been.calledOnce; }); - - it('should disable Mock Data Generator button when hasSchemaAnalysisData is false', async function () { - mockUseAssignment.returns({ - assignment: { - assignmentData: { - variant: 'mockDataGeneratorVariant', - }, - }, - }); - - await renderCollectionHeaderActions( - { - namespace: 'test.collection', - isReadonly: false, - hasSchemaAnalysisData: false, - }, - {}, - atlasConnectionInfo - ); - - const button = screen.getByTestId( - 'collection-header-generate-mock-data-button' - ); - expect(button).to.have.attribute('aria-disabled', 'true'); - }); - - it('should enable Mock Data Generator button when hasSchemaAnalysisData is true', async function () { - mockUseAssignment.returns({ - assignment: { - assignmentData: { - variant: 'mockDataGeneratorVariant', - }, - }, - }); - - await renderCollectionHeaderActions( - { - namespace: 'test.collection', - isReadonly: false, - hasSchemaAnalysisData: true, - }, - {}, - atlasConnectionInfo - ); - - const button = screen.getByTestId( - 'collection-header-generate-mock-data-button' - ); - expect(button).to.not.have.attribute('aria-disabled', 'true'); - }); - - it('should not show Mock Data Generator button for collections with excessive nesting depth', async function () { - mockUseAssignment.returns({ - assignment: { - assignmentData: { - variant: 'mockDataGeneratorVariant', - }, - }, - }); - - await renderCollectionHeaderActions( - { - namespace: 'test.collection', - isReadonly: false, - hasSchemaAnalysisData: true, - analyzedSchemaDepth: 4, // Exceeds the limit of 3 - }, - {}, - atlasConnectionInfo - ); - - expect( - screen.queryByTestId('collection-header-generate-mock-data-button') - ).to.not.exist; - }); }); }); diff --git a/packages/compass-collection/src/components/collection-header/collection-header.spec.tsx b/packages/compass-collection/src/components/collection-header/collection-header.spec.tsx index dba3a7760a3..937a5d3f163 100644 --- a/packages/compass-collection/src/components/collection-header/collection-header.spec.tsx +++ b/packages/compass-collection/src/components/collection-header/collection-header.spec.tsx @@ -3,6 +3,7 @@ import type { ComponentProps } from 'react'; import React from 'react'; import { renderWithConnections, + renderWithActiveConnection, screen, cleanup, within, @@ -16,19 +17,26 @@ import { type WorkspacesService, } from '@mongodb-js/compass-workspaces/provider'; import { MockDataGeneratorStep } from '../mock-data-generator-modal/types'; +import { SCHEMA_ANALYSIS_STATE_COMPLETE } from '../../schema-analysis-types'; +import { CompassExperimentationProvider } from '@mongodb-js/compass-telemetry'; +import type { ConnectionInfo } from '@mongodb-js/compass-connections/provider'; import Sinon from 'sinon'; function renderCollectionHeader( props: Partial> = {}, - workspaceService: Partial = {} + workspaceService: Partial = {}, + stateOverrides: any = {} ) { - const mockStore = createStore(() => ({ + const defaultState = { mockDataGenerator: { isModalOpen: false, currentStep: MockDataGeneratorStep.SCHEMA_CONFIRMATION, }, - })); + ...stateOverrides, + }; + + const mockStore = createStore(() => defaultState); return renderWithConnections( @@ -325,4 +333,259 @@ describe('CollectionHeader [Component]', function () { expect(screen.getByTestId('collection-header')).to.exist; expect(screen.getByTestId('collection-header-actions')).to.exist; }); + + describe('Mock Data Generator integration', function () { + let mockUseAssignment: Sinon.SinonStub; + + beforeEach(function () { + // Mock the useAssignment hook from compass-experimentation + mockUseAssignment = Sinon.stub().returns({ + assignment: { + assignmentData: { + variant: 'mockDataGeneratorVariant', + }, + }, + }); + }); + + afterEach(function () { + Sinon.restore(); + }); + + const atlasConnectionInfo: ConnectionInfo = { + id: 'test-atlas-connection', + connectionOptions: { + connectionString: 'mongodb://localhost:27017', + }, + atlasMetadata: { + orgId: 'test-org', + projectId: 'test-project', + clusterName: 'test-cluster', + clusterUniqueId: 'test-cluster-unique-id', + clusterType: 'REPLICASET', + clusterState: 'IDLE', + metricsId: 'test-metrics-id', + metricsType: 'replicaSet', + regionalBaseUrl: null, + instanceSize: 'M10', + supports: { + globalWrites: false, + rollingIndexes: true, + }, + }, + }; + + function renderCollectionHeaderWithExperimentation( + props: Partial> = {}, + workspaceService: Partial = {}, + stateOverrides: any = {}, + connectionInfo?: ConnectionInfo + ) { + const defaultState = { + mockDataGenerator: { + isModalOpen: false, + currentStep: MockDataGeneratorStep.SCHEMA_CONFIRMATION, + }, + ...stateOverrides, + }; + + const mockStore = createStore(() => defaultState); + + return renderWithActiveConnection( + + + + + + + , + connectionInfo + ); + } + + it('should show Mock Data Generator button when all conditions are met', async function () { + await renderCollectionHeaderWithExperimentation( + { + isAtlas: true, // Atlas environment + isReadonly: false, // Not readonly + namespace: 'test.collection', + }, + {}, + { + schemaAnalysis: { + status: SCHEMA_ANALYSIS_STATE_COMPLETE, + processedSchema: { + field1: { type: 'String', sample_values: ['value1'] }, + }, + schemaMetadata: { + maxNestingDepth: 2, // Below the limit of 4 + }, + }, + }, + atlasConnectionInfo + ); + + expect(screen.getByTestId('collection-header-generate-mock-data-button')) + .to.exist; + expect( + screen.getByTestId('collection-header-generate-mock-data-button') + ).to.not.have.attribute('aria-disabled', 'true'); + }); + + it('should disable Mock Data Generator button when collection has no schema analysis data', async function () { + await renderCollectionHeaderWithExperimentation( + { + isAtlas: true, + isReadonly: false, + namespace: 'test.collection', + }, + {}, + { + schemaAnalysis: { + status: SCHEMA_ANALYSIS_STATE_COMPLETE, + processedSchema: {}, // Empty schema + schemaMetadata: { + maxNestingDepth: 2, + }, + }, + }, + atlasConnectionInfo + ); + + const button = screen.getByTestId( + 'collection-header-generate-mock-data-button' + ); + expect(button).to.exist; + expect(button).to.have.attribute('aria-disabled', 'true'); + }); + + it('should not show Mock Data Generator button for collections with excessive nesting depth', async function () { + await renderCollectionHeaderWithExperimentation( + { + isAtlas: true, + isReadonly: false, + namespace: 'test.collection', + }, + {}, + { + schemaAnalysis: { + status: SCHEMA_ANALYSIS_STATE_COMPLETE, + processedSchema: { + field1: { type: 'String', sample_values: ['value1'] }, + }, + schemaMetadata: { + maxNestingDepth: 4, // Exceeds the limit + }, + }, + }, + atlasConnectionInfo + ); + + expect( + screen.queryByTestId('collection-header-generate-mock-data-button') + ).to.not.exist; + }); + + it('should not show Mock Data Generator button for readonly collections (views)', async function () { + await renderCollectionHeaderWithExperimentation( + { + isAtlas: true, + isReadonly: true, // Readonly (view) + namespace: 'test.view', + }, + {}, + { + schemaAnalysis: { + status: SCHEMA_ANALYSIS_STATE_COMPLETE, + processedSchema: { + field1: { type: 'String', sample_values: ['value1'] }, + }, + schemaMetadata: { + maxNestingDepth: 2, + }, + }, + }, + atlasConnectionInfo + ); + + expect( + screen.queryByTestId('collection-header-generate-mock-data-button') + ).to.not.exist; + }); + + it('should not show Mock Data Generator button in non-Atlas environments', async function () { + await renderCollectionHeaderWithExperimentation( + { + isAtlas: false, // Not Atlas + isReadonly: false, + namespace: 'test.collection', + }, + {}, + { + schemaAnalysis: { + status: SCHEMA_ANALYSIS_STATE_COMPLETE, + processedSchema: { + field1: { type: 'String', sample_values: ['value1'] }, + }, + schemaMetadata: { + maxNestingDepth: 2, + }, + }, + } + // Don't pass atlasConnectionInfo to simulate non-Atlas environment + ); + + expect( + screen.queryByTestId('collection-header-generate-mock-data-button') + ).to.not.exist; + }); + + it('should not show Mock Data Generator button when not in treatment variant', async function () { + mockUseAssignment.returns({ + assignment: { + assignmentData: { + variant: 'control', + }, + }, + }); + + await renderCollectionHeaderWithExperimentation( + { + isAtlas: true, + isReadonly: false, + namespace: 'test.collection', + }, + {}, + { + schemaAnalysis: { + status: SCHEMA_ANALYSIS_STATE_COMPLETE, + processedSchema: { + field1: { type: 'String', sample_values: ['value1'] }, + }, + schemaMetadata: { + maxNestingDepth: 2, + }, + }, + }, + atlasConnectionInfo + ); + + expect( + screen.queryByTestId('collection-header-generate-mock-data-button') + ).to.not.exist; + }); + }); }); From 0a0ab7baab50cad68da3879e44ca139fa58b7413 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Thu, 4 Sep 2025 12:39:23 -0500 Subject: [PATCH 06/11] Address comment --- .../collection-header-actions/collection-header-actions.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 77a5b7efb8f..25a73d2602e 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 @@ -22,7 +22,7 @@ import { /** * Maximum allowed nesting depth for collections to show Mock Data Generator */ -const MAX_COLLECTION_NESTING_DEPTH = 4; +const MAX_COLLECTION_NESTING_DEPTH = 3; const collectionHeaderActionsStyles = css({ display: 'flex', @@ -94,7 +94,7 @@ const CollectionHeaderActions: React.FunctionComponent< atlasMetadata && // Only show in Atlas !isReadonly && // Don't show for readonly collections (views) !sourceName && // sourceName indicates it's a view - analyzedSchemaDepth < MAX_COLLECTION_NESTING_DEPTH; // Filter out overly nested collections + analyzedSchemaDepth <= MAX_COLLECTION_NESTING_DEPTH; // Filter out overly nested collections return (
Date: Mon, 8 Sep 2025 13:22:23 -0400 Subject: [PATCH 07/11] Add getAssignment to experimentation interface --- .../src/stores/collection-tab.spec.ts | 156 +++++++++++++++++- .../src/stores/collection-tab.ts | 42 ++++- .../src/experimentation-provider.tsx | 20 ++- packages/compass-telemetry/src/provider.tsx | 11 +- 4 files changed, 213 insertions(+), 16 deletions(-) diff --git a/packages/compass-collection/src/stores/collection-tab.spec.ts b/packages/compass-collection/src/stores/collection-tab.spec.ts index 6de48f4426f..ad020ee1342 100644 --- a/packages/compass-collection/src/stores/collection-tab.spec.ts +++ b/packages/compass-collection/src/stores/collection-tab.spec.ts @@ -7,12 +7,38 @@ import Sinon from 'sinon'; import AppRegistry from '@mongodb-js/compass-app-registry'; import { expect } from 'chai'; import type { workspacesServiceLocator } from '@mongodb-js/compass-workspaces/provider'; -import type { experimentationServiceLocator } from '@mongodb-js/compass-telemetry/provider'; +import type { ExperimentationServices } from '@mongodb-js/compass-telemetry/provider'; import type { connectionInfoRefLocator } from '@mongodb-js/compass-connections/provider'; import { createNoopLogger } from '@mongodb-js/compass-logging/provider'; import { ReadOnlyPreferenceAccess } from 'compass-preferences-model/provider'; -import { ExperimentTestName } from '@mongodb-js/compass-telemetry/provider'; +import { + ExperimentTestName, + ExperimentTestGroup, +} from '@mongodb-js/compass-telemetry/provider'; import { type CollectionMetadata } from 'mongodb-collection-model'; +import type { types } from '@mongodb-js/mdb-experiment-js'; + +// Helper function to create proper mock assignment objects for testing +const createMockAssignment = ( + variant: ExperimentTestGroup +): types.SDKAssignment => ({ + assignmentData: { + variant, + isInSample: true, + }, + experimentData: { + assignmentDate: '2024-01-01T00:00:00Z', + entityType: 'USER' as types.EntityType, + id: 'test-assignment-id', + tag: 'test-tag', + testGroupId: 'test-group-id', + entityId: 'test-user-id', + testId: 'test-id', + testName: ExperimentTestName.mockDataGenerator, + testGroupDatabaseId: 'test-group-db-id', + meta: { isLaunchedExperiment: true }, + }, +}); const defaultMetadata = { namespace: 'test.foo', @@ -71,9 +97,7 @@ describe('Collection Tab Content store', function () { const configureStore = async ( options: Partial = {}, workspaces: Partial> = {}, - experimentationServices: Partial< - ReturnType - > = {}, + experimentationServices: Partial = {}, connectionInfoRef: Partial< ReturnType > = {}, @@ -242,9 +266,21 @@ describe('Collection Tab Content store', function () { describe('schema analysis on collection load', function () { it('should start schema analysis if collection is not read-only and not time-series', async function () { - await configureStore(); + const getAssignment = sandbox.spy(() => + Promise.resolve( + createMockAssignment(ExperimentTestGroup.mockDataGeneratorVariant) + ) + ); + const assignExperiment = sandbox.spy(() => Promise.resolve(null)); + + await configureStore(undefined, undefined, { + getAssignment, + assignExperiment, + }); - expect(analyzeCollectionSchemaStub).to.have.been.calledOnce; + await waitFor(() => { + expect(analyzeCollectionSchemaStub).to.have.been.calledOnce; + }); }); it('should not start schema analysis if collection is read-only', async function () { @@ -274,5 +310,111 @@ describe('Collection Tab Content store', function () { expect(analyzeCollectionSchemaStub).to.not.have.been.called; }); + + it('should not start schema analysis in non-Atlas environments', async function () { + const getAssignment = sandbox.spy(() => Promise.resolve(null)); + const assignExperiment = sandbox.spy(() => Promise.resolve(null)); + const mockConnectionInfoRef = { + current: { + id: 'test-connection', + title: 'Test Connection', + connectionOptions: { + connectionString: 'mongodb://localhost:27017', + }, + // No atlasMetadata (non-Atlas environment) + }, + }; + + await configureStore( + undefined, + undefined, + { getAssignment, assignExperiment }, + mockConnectionInfoRef + ); + + await waitFor(() => { + expect(getAssignment).to.have.been.calledOnceWith( + ExperimentTestName.mockDataGenerator, + false + ); + }); + + // Wait a bit to ensure schema analysis would not have been called + await new Promise((resolve) => setTimeout(resolve, 50)); + expect(analyzeCollectionSchemaStub).to.not.have.been.called; + }); + + it('should start schema analysis in Atlas when user is in treatment variant', async function () { + const getAssignment = sandbox.spy(() => + Promise.resolve( + createMockAssignment(ExperimentTestGroup.mockDataGeneratorVariant) + ) + ); + const assignExperiment = sandbox.spy(() => Promise.resolve(null)); + + await configureStore( + undefined, + undefined, + { getAssignment, assignExperiment }, + mockAtlasConnectionInfo + ); + + await waitFor(() => { + expect(getAssignment).to.have.been.calledOnceWith( + ExperimentTestName.mockDataGenerator, + false // Don't track "Experiment Viewed" event + ); + expect(analyzeCollectionSchemaStub).to.have.been.calledOnce; + }); + }); + + it('should not start schema analysis in Atlas when user is in control variant', async function () { + const getAssignment = sandbox.spy(() => + Promise.resolve( + createMockAssignment(ExperimentTestGroup.mockDataGeneratorControl) + ) + ); + const assignExperiment = sandbox.spy(() => Promise.resolve(null)); + + await configureStore( + undefined, + undefined, + { getAssignment, assignExperiment }, + mockAtlasConnectionInfo + ); + + await waitFor(() => { + expect(getAssignment).to.have.been.calledOnceWith( + ExperimentTestName.mockDataGenerator, + false + ); + }); + + // Wait a bit to ensure schema analysis would not have been called + await new Promise((resolve) => setTimeout(resolve, 50)); + expect(analyzeCollectionSchemaStub).to.not.have.been.called; + }); + + it('should not start schema analysis when getAssignment fails', async function () { + const getAssignment = sandbox.spy(() => + Promise.reject(new Error('Assignment failed')) + ); + const assignExperiment = sandbox.spy(() => Promise.resolve(null)); + + await configureStore( + undefined, + undefined, + { getAssignment, assignExperiment }, + mockAtlasConnectionInfo + ); + + await waitFor(() => { + expect(getAssignment).to.have.been.calledOnce; + }); + + // Wait a bit to ensure schema analysis would not have been called + await new Promise((resolve) => setTimeout(resolve, 50)); + expect(analyzeCollectionSchemaStub).to.not.have.been.called; + }); }); }); diff --git a/packages/compass-collection/src/stores/collection-tab.ts b/packages/compass-collection/src/stores/collection-tab.ts index 48b4240ff63..4d50ca104ed 100644 --- a/packages/compass-collection/src/stores/collection-tab.ts +++ b/packages/compass-collection/src/stores/collection-tab.ts @@ -13,7 +13,7 @@ import { MockDataGeneratorStep } from '../components/mock-data-generator-modal/t import type { Collection } from '@mongodb-js/compass-app-stores/provider'; import type { ActivateHelpers } from '@mongodb-js/compass-app-registry'; import type { workspacesServiceLocator } from '@mongodb-js/compass-workspaces/provider'; -import type { experimentationServiceLocator } from '@mongodb-js/compass-telemetry/provider'; +import type { ExperimentationServices } from '@mongodb-js/compass-telemetry/provider'; import type { connectionInfoRefLocator } from '@mongodb-js/compass-connections/provider'; import type { Logger } from '@mongodb-js/compass-logging/provider'; import type { AtlasAiService } from '@mongodb-js/compass-generative-ai/provider'; @@ -21,7 +21,10 @@ import { isAIFeatureEnabled, type PreferencesAccess, } from 'compass-preferences-model/provider'; -import { ExperimentTestName } from '@mongodb-js/compass-telemetry/provider'; +import { + ExperimentTestName, + ExperimentTestGroup, +} from '@mongodb-js/compass-telemetry/provider'; import { SCHEMA_ANALYSIS_STATE_INITIAL } from '../schema-analysis-types'; export type CollectionTabOptions = { @@ -46,7 +49,7 @@ export type CollectionTabServices = { localAppRegistry: AppRegistry; atlasAiService: AtlasAiService; workspaces: ReturnType; - experimentationServices: ReturnType; + experimentationServices: ExperimentationServices; connectionInfoRef: ReturnType; logger: Logger; preferences: PreferencesAccess; @@ -153,8 +156,37 @@ export function activatePlugin( } if (!metadata.isReadonly && !metadata.isTimeSeries) { - // TODO: Consider checking experiment variant - void store.dispatch(analyzeCollectionSchema()); + // Check experiment variant before running schema analysis + // Only run schema analysis if user is in treatment variant + const shouldRunSchemaAnalysis = async () => { + try { + const assignment = await experimentationServices.getAssignment( + ExperimentTestName.mockDataGenerator, + false // Don't track "Experiment Viewed" event here + ); + return ( + assignment?.assignmentData?.variant === + ExperimentTestGroup.mockDataGeneratorVariant + ); + } catch (error) { + // On error, default to not running schema analysis + logger.debug( + 'Failed to get Mock Data Generator experiment assignment', + { + experiment: ExperimentTestName.mockDataGenerator, + namespace: namespace, + error: error instanceof Error ? error.message : String(error), + } + ); + return false; + } + }; + + void shouldRunSchemaAnalysis().then((shouldRun) => { + if (shouldRun) { + void store.dispatch(analyzeCollectionSchema()); + } + }); } }); diff --git a/packages/compass-telemetry/src/experimentation-provider.tsx b/packages/compass-telemetry/src/experimentation-provider.tsx index 3055fd255a3..6bdbda73647 100644 --- a/packages/compass-telemetry/src/experimentation-provider.tsx +++ b/packages/compass-telemetry/src/experimentation-provider.tsx @@ -14,9 +14,16 @@ type AssignExperimentFn = ( options?: types.AssignOptions ) => Promise; +type GetAssignmentFn = ( + experimentName: ExperimentTestName, + trackIsInSample: boolean, + options?: types.GetAssignmentOptions +) => Promise | null>; + interface CompassExperimentationProviderContextValue { useAssignment: UseAssignmentHook; assignExperiment: AssignExperimentFn; + getAssignment: GetAssignmentFn; } const initialContext: CompassExperimentationProviderContextValue = { @@ -33,6 +40,9 @@ const initialContext: CompassExperimentationProviderContextValue = { assignExperiment() { return Promise.resolve(null); }, + getAssignment() { + return Promise.resolve(null); + }, }; export const ExperimentationContext = @@ -43,12 +53,18 @@ export const CompassExperimentationProvider: React.FC<{ children: React.ReactNode; useAssignment: UseAssignmentHook; assignExperiment: AssignExperimentFn; -}> = ({ children, useAssignment, assignExperiment }) => { + getAssignment: GetAssignmentFn; +}> = ({ children, useAssignment, assignExperiment, getAssignment }) => { // Use useRef to keep the functions up-to-date; Use mutation pattern to maintain the // same object reference to prevent unnecessary re-renders of consuming components - const { current: contextValue } = useRef({ useAssignment, assignExperiment }); + const { current: contextValue } = useRef({ + useAssignment, + assignExperiment, + getAssignment, + }); contextValue.useAssignment = useAssignment; contextValue.assignExperiment = assignExperiment; + contextValue.getAssignment = getAssignment; return ( diff --git a/packages/compass-telemetry/src/provider.tsx b/packages/compass-telemetry/src/provider.tsx index 301222d3838..4446dbbfb03 100644 --- a/packages/compass-telemetry/src/provider.tsx +++ b/packages/compass-telemetry/src/provider.tsx @@ -54,13 +54,20 @@ export interface ExperimentationServices { experimentName: ExperimentTestName, options?: types.AssignOptions ) => Promise; + getAssignment: ( + experimentName: ExperimentTestName, + trackIsInSample: boolean, + options?: types.GetAssignmentOptions + ) => Promise | null>; } // Service locator for experimentation services (non-component access) export const experimentationServiceLocator = createServiceLocator( function useExperimentationServices(): ExperimentationServices { - const { assignExperiment } = useContext(ExperimentationContext); - return { assignExperiment }; + const { assignExperiment, getAssignment } = useContext( + ExperimentationContext + ); + return { assignExperiment, getAssignment }; }, 'experimentationServiceLocator' ); From 137267c76949ee17510fad26b11dce31206205eb Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Mon, 8 Sep 2025 13:45:52 -0400 Subject: [PATCH 08/11] Dependency updates --- package-lock.json | 2 ++ packages/compass-collection/package.json | 1 + .../collection-header-actions.spec.tsx | 1 + .../src/components/collection-header/collection-header.spec.tsx | 1 + 4 files changed, 5 insertions(+) diff --git a/package-lock.json b/package-lock.json index 1a10f197185..b940df8c5d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47722,6 +47722,7 @@ }, "devDependencies": { "@mongodb-js/eslint-config-compass": "^1.4.8", + "@mongodb-js/mdb-experiment-js": "1.9.0", "@mongodb-js/mocha-config-compass": "^1.7.1", "@mongodb-js/prettier-config-compass": "^1.2.8", "@mongodb-js/testing-library-compass": "^1.3.11", @@ -61182,6 +61183,7 @@ "@mongodb-js/compass-workspaces": "^0.53.1", "@mongodb-js/connection-info": "^0.18.0", "@mongodb-js/eslint-config-compass": "^1.4.8", + "@mongodb-js/mdb-experiment-js": "1.9.0", "@mongodb-js/mocha-config-compass": "^1.7.1", "@mongodb-js/mongodb-constants": "^0.14.0", "@mongodb-js/prettier-config-compass": "^1.2.8", diff --git a/packages/compass-collection/package.json b/packages/compass-collection/package.json index 588c09aedbe..31bdfbcd877 100644 --- a/packages/compass-collection/package.json +++ b/packages/compass-collection/package.json @@ -73,6 +73,7 @@ }, "devDependencies": { "@mongodb-js/eslint-config-compass": "^1.4.8", + "@mongodb-js/mdb-experiment-js": "1.9.0", "@mongodb-js/mocha-config-compass": "^1.7.1", "@mongodb-js/prettier-config-compass": "^1.2.8", "@mongodb-js/testing-library-compass": "^1.3.11", diff --git a/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx b/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx index f10bd1646fd..c325375ba4a 100644 --- a/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx +++ b/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx @@ -48,6 +48,7 @@ describe('CollectionHeaderActions [Component]', function () { Date: Mon, 8 Sep 2025 14:19:32 -0400 Subject: [PATCH 09/11] Hide button tooltip while analysis in progress --- .../collection-header-actions.spec.tsx | 1 + .../collection-header-actions.tsx | 11 ++++++++++- .../collection-header/collection-header.tsx | 9 ++++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx b/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx index c325375ba4a..1e7d45349b1 100644 --- a/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx +++ b/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx @@ -60,6 +60,7 @@ describe('CollectionHeaderActions [Component]', function () { onOpenMockDataModal={sinon.stub()} hasSchemaAnalysisData={true} analyzedSchemaDepth={2} + schemaAnalysisStatus="complete" {...props} /> 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 25a73d2602e..c69976808d2 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 @@ -18,6 +18,10 @@ import { ExperimentTestName, ExperimentTestGroup, } from '@mongodb-js/compass-telemetry/provider'; +import { + SCHEMA_ANALYSIS_STATE_ANALYZING, + type SchemaAnalysisStatus, +} from '../../schema-analysis-types'; /** * Maximum allowed nesting depth for collections to show Mock Data Generator @@ -54,6 +58,7 @@ type CollectionHeaderActionsProps = { onOpenMockDataModal: () => void; hasSchemaAnalysisData: boolean; analyzedSchemaDepth: number; + schemaAnalysisStatus: SchemaAnalysisStatus | null; }; const CollectionHeaderActions: React.FunctionComponent< @@ -67,6 +72,7 @@ const CollectionHeaderActions: React.FunctionComponent< onOpenMockDataModal, hasSchemaAnalysisData, analyzedSchemaDepth, + schemaAnalysisStatus, }: CollectionHeaderActionsProps) => { const connectionInfo = useConnectionInfo(); const { id: connectionId, atlasMetadata } = connectionInfo; @@ -118,7 +124,10 @@ const CollectionHeaderActions: React.FunctionComponent< )} {shouldShowMockDataButton && (
@@ -201,6 +207,7 @@ const mapStateToProps = (state: CollectionState) => { schemaAnalysis && schemaAnalysis.status === SCHEMA_ANALYSIS_STATE_COMPLETE ? schemaAnalysis.schemaMetadata.maxNestingDepth : 0, + schemaAnalysisStatus: schemaAnalysis?.status || null, }; }; From e37fc166309912f99ede4799b0622496cf63455e Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Mon, 8 Sep 2025 16:31:03 -0400 Subject: [PATCH 10/11] Address comment to add tooltip --- .../collection-header-actions.spec.tsx | 29 +++++++++++++++++++ .../collection-header-actions.tsx | 22 +++++++++----- .../collection-header.spec.tsx | 10 ++++--- 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx b/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx index 1e7d45349b1..1f98a73199d 100644 --- a/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx +++ b/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx @@ -265,5 +265,34 @@ describe('CollectionHeaderActions [Component]', function () { expect(onOpenMockDataModal).to.have.been.calledOnce; }); + + it('should disable button for deeply nested collections', async function () { + mockUseAssignment.returns({ + assignment: { + assignmentData: { + variant: 'mockDataGeneratorVariant', // Treatment variant + }, + }, + }); + + await renderCollectionHeaderActions( + { + namespace: 'test.collection', + isReadonly: false, + hasSchemaAnalysisData: true, + analyzedSchemaDepth: 5, // Exceeds MAX_COLLECTION_NESTING_DEPTH (3) + schemaAnalysisStatus: 'complete', + onOpenMockDataModal: sinon.stub(), + }, + {}, + atlasConnectionInfo + ); + + const button = screen.getByTestId( + 'collection-header-generate-mock-data-button' + ); + expect(button).to.exist; + expect(button).to.have.attribute('aria-disabled', 'true'); + }); }); }); 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 c69976808d2..e8204823d5c 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 @@ -99,8 +99,14 @@ const CollectionHeaderActions: React.FunctionComponent< isInMockDataTreatmentVariant && atlasMetadata && // Only show in Atlas !isReadonly && // Don't show for readonly collections (views) - !sourceName && // sourceName indicates it's a view - analyzedSchemaDepth <= MAX_COLLECTION_NESTING_DEPTH; // Filter out overly nested collections + !sourceName; // sourceName indicates it's a view + + const exceedsMaxNestingDepth = + analyzedSchemaDepth > MAX_COLLECTION_NESTING_DEPTH; + + const isCollectionEmpty = + !hasSchemaAnalysisData && + schemaAnalysisStatus !== SCHEMA_ANALYSIS_STATE_ANALYZING; return (
} > - Please add data to your collection to generate similar mock documents + {exceedsMaxNestingDepth && + 'At this time we are unable to generate mock data for collections that have deeply nested documents'} + {isCollectionEmpty && + 'Please add data to your collection to generate similar mock documents'} )} {atlasMetadata && ( diff --git a/packages/compass-collection/src/components/collection-header/collection-header.spec.tsx b/packages/compass-collection/src/components/collection-header/collection-header.spec.tsx index b71f49ca4cc..82553cc22af 100644 --- a/packages/compass-collection/src/components/collection-header/collection-header.spec.tsx +++ b/packages/compass-collection/src/components/collection-header/collection-header.spec.tsx @@ -473,7 +473,7 @@ describe('CollectionHeader [Component]', function () { expect(button).to.have.attribute('aria-disabled', 'true'); }); - it('should not show Mock Data Generator button for collections with excessive nesting depth', async function () { + it('should disable Mock Data Generator button for collections with excessive nesting depth', async function () { await renderCollectionHeaderWithExperimentation( { isAtlas: true, @@ -495,9 +495,11 @@ describe('CollectionHeader [Component]', function () { atlasConnectionInfo ); - expect( - screen.queryByTestId('collection-header-generate-mock-data-button') - ).to.not.exist; + const button = screen.getByTestId( + 'collection-header-generate-mock-data-button' + ); + expect(button).to.exist; + expect(button).to.have.attribute('aria-disabled', 'true'); }); it('should not show Mock Data Generator button for readonly collections (views)', async function () { From e0c9a0acb3feb870185ab7afe818fe79fe143c12 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Mon, 8 Sep 2025 16:44:59 -0400 Subject: [PATCH 11/11] Add thunk action abort signal cancellation --- .../src/modules/collection-tab.ts | 77 ++++++++++++++++++- .../src/stores/collection-tab.spec.ts | 26 ++++++- .../src/stores/collection-tab.ts | 10 ++- 3 files changed, 110 insertions(+), 3 deletions(-) diff --git a/packages/compass-collection/src/modules/collection-tab.ts b/packages/compass-collection/src/modules/collection-tab.ts index 7a001624dd4..e1f3afa2eac 100644 --- a/packages/compass-collection/src/modules/collection-tab.ts +++ b/packages/compass-collection/src/modules/collection-tab.ts @@ -79,6 +79,7 @@ type CollectionThunkAction = ThunkAction< preferences: PreferencesAccess; connectionInfoRef: ConnectionInfoRef; fakerSchemaGenerationAbortControllerRef: { current?: AbortController }; + schemaAnalysisAbortControllerRef: { current?: AbortController }; }, A >; @@ -101,6 +102,7 @@ export enum CollectionActions { SchemaAnalysisStarted = 'compass-collection/SchemaAnalysisStarted', SchemaAnalysisFinished = 'compass-collection/SchemaAnalysisFinished', SchemaAnalysisFailed = 'compass-collection/SchemaAnalysisFailed', + SchemaAnalysisCanceled = 'compass-collection/SchemaAnalysisCanceled', SchemaAnalysisReset = 'compass-collection/SchemaAnalysisReset', MockDataGeneratorModalOpened = 'compass-collection/MockDataGeneratorModalOpened', MockDataGeneratorModalClosed = 'compass-collection/MockDataGeneratorModalClosed', @@ -139,6 +141,10 @@ interface SchemaAnalysisFailedAction { error: Error; } +interface SchemaAnalysisCanceledAction { + type: CollectionActions.SchemaAnalysisCanceled; +} + interface MockDataGeneratorModalOpenedAction { type: CollectionActions.MockDataGeneratorModalOpened; } @@ -263,6 +269,20 @@ const reducer: Reducer = ( }; } + if ( + isAction( + action, + CollectionActions.SchemaAnalysisCanceled + ) + ) { + return { + ...state, + schemaAnalysis: { + status: SCHEMA_ANALYSIS_STATE_INITIAL, + }, + }; + } + if ( isAction( action, @@ -524,7 +544,11 @@ export const openMockDataGeneratorModal = (): CollectionThunkAction< export const analyzeCollectionSchema = (): CollectionThunkAction< Promise > => { - return async (dispatch, getState, { dataService, preferences, logger }) => { + return async ( + dispatch, + getState, + { dataService, preferences, logger, schemaAnalysisAbortControllerRef } + ) => { const { schemaAnalysis, namespace } = getState(); const analysisStatus = schemaAnalysis.status; if (analysisStatus === SCHEMA_ANALYSIS_STATE_ANALYZING) { @@ -534,6 +558,10 @@ export const analyzeCollectionSchema = (): CollectionThunkAction< return; } + // Create abort controller for this analysis + const abortController = new AbortController(); + schemaAnalysisAbortControllerRef.current = abortController; + try { logger.debug('Schema analysis started.'); @@ -552,8 +580,15 @@ export const analyzeCollectionSchema = (): CollectionThunkAction< driverOptions, { fallbackReadPreference: 'secondaryPreferred', + abortSignal: abortController.signal, } ); + + // Check if analysis was aborted after sampling + if (abortController.signal.aborted) { + logger.debug('Schema analysis was aborted during sampling'); + return; + } if (sampleDocuments.length === 0) { logger.debug(NO_DOCUMENTS_ERROR); dispatch({ @@ -565,6 +600,13 @@ export const analyzeCollectionSchema = (): CollectionThunkAction< // Analyze sampled documents const schemaAccessor = await analyzeDocuments(sampleDocuments); + + // Check if analysis was aborted after document analysis + if (abortController.signal.aborted) { + logger.debug('Schema analysis was aborted during document analysis'); + return; + } + const schema = await schemaAccessor.getInternalSchema(); // Filter out internal fields from the schema @@ -583,6 +625,13 @@ export const analyzeCollectionSchema = (): CollectionThunkAction< maxNestingDepth, validationRules, }; + + // Final check before dispatching results + if (abortController.signal.aborted) { + logger.debug('Schema analysis was aborted before completion'); + return; + } + dispatch({ type: CollectionActions.SchemaAnalysisFinished, processedSchema, @@ -590,6 +639,15 @@ export const analyzeCollectionSchema = (): CollectionThunkAction< schemaMetadata, }); } catch (err: any) { + // Check if the error is due to cancellation + if (isCancelError(err) || abortController.signal.aborted) { + logger.debug('Schema analysis was aborted'); + dispatch({ + type: CollectionActions.SchemaAnalysisCanceled, + }); + return; + } + logger.log.error( mongoLogId(1_001_000_363), 'Collection', @@ -603,6 +661,23 @@ export const analyzeCollectionSchema = (): CollectionThunkAction< type: CollectionActions.SchemaAnalysisFailed, error: err as Error, }); + } finally { + // Clean up abort controller + schemaAnalysisAbortControllerRef.current = undefined; + } + }; +}; + +export const cancelSchemaAnalysis = (): CollectionThunkAction => { + return ( + _dispatch, + _getState, + { schemaAnalysisAbortControllerRef, logger } + ) => { + if (schemaAnalysisAbortControllerRef.current) { + logger.debug('Canceling schema analysis'); + schemaAnalysisAbortControllerRef.current.abort(); + schemaAnalysisAbortControllerRef.current = undefined; } }; }; diff --git a/packages/compass-collection/src/stores/collection-tab.spec.ts b/packages/compass-collection/src/stores/collection-tab.spec.ts index ad020ee1342..bcbcb52f763 100644 --- a/packages/compass-collection/src/stores/collection-tab.spec.ts +++ b/packages/compass-collection/src/stores/collection-tab.spec.ts @@ -134,7 +134,7 @@ describe('Collection Tab Content store', function () { logger, preferences, }, - { on() {}, cleanup() {} } as any + { on() {}, cleanup() {}, addCleanup() {} } as any )); await waitFor(() => { expect(store.getState()) @@ -417,4 +417,28 @@ describe('Collection Tab Content store', function () { expect(analyzeCollectionSchemaStub).to.not.have.been.called; }); }); + + describe('schema analysis cancellation', function () { + it('should cancel schema analysis when cancelSchemaAnalysis is dispatched', async function () { + const getAssignment = sandbox.spy(() => + Promise.resolve( + createMockAssignment(ExperimentTestGroup.mockDataGeneratorVariant) + ) + ); + const assignExperiment = sandbox.spy(() => Promise.resolve(null)); + + const store = await configureStore(undefined, undefined, { + getAssignment, + assignExperiment, + }); + + // Dispatch cancel action + store.dispatch(collectionTabModule.cancelSchemaAnalysis() as any); + + // Verify the state is reset to initial + expect((store.getState() as any).schemaAnalysis.status).to.equal( + 'initial' + ); + }); + }); }); diff --git a/packages/compass-collection/src/stores/collection-tab.ts b/packages/compass-collection/src/stores/collection-tab.ts index 4d50ca104ed..b49f4e90845 100644 --- a/packages/compass-collection/src/stores/collection-tab.ts +++ b/packages/compass-collection/src/stores/collection-tab.ts @@ -7,6 +7,7 @@ import reducer, { selectTab, collectionMetadataFetched, analyzeCollectionSchema, + cancelSchemaAnalysis, } from '../modules/collection-tab'; import { MockDataGeneratorStep } from '../components/mock-data-generator-modal/types'; @@ -58,7 +59,7 @@ export type CollectionTabServices = { export function activatePlugin( { namespace, editViewName, tabId }: CollectionTabOptions, services: CollectionTabServices, - { on, cleanup }: ActivateHelpers + { on, cleanup, addCleanup }: ActivateHelpers ): { store: ReturnType; deactivate: () => void; @@ -84,6 +85,9 @@ export function activatePlugin( const fakerSchemaGenerationAbortControllerRef = { current: undefined, }; + const schemaAnalysisAbortControllerRef = { + current: undefined, + }; const store = createStore( reducer, { @@ -113,6 +117,7 @@ export function activatePlugin( logger, preferences, fakerSchemaGenerationAbortControllerRef, + schemaAnalysisAbortControllerRef, }) ) ); @@ -190,6 +195,9 @@ export function activatePlugin( } }); + // Cancel schema analysis when plugin is deactivated + addCleanup(() => store.dispatch(cancelSchemaAnalysis())); + return { store, deactivate: cleanup,