From 112ee188867202de4007c09bfc135edf40819c5e Mon Sep 17 00:00:00 2001 From: Nataly Carbonell Date: Thu, 25 Sep 2025 14:43:45 -0400 Subject: [PATCH 1/4] document count screen --- .../document-count-screen.tsx | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 packages/compass-collection/src/components/mock-data-generator-modal/document-count-screen.tsx diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/document-count-screen.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/document-count-screen.tsx new file mode 100644 index 00000000000..1649b16cd51 --- /dev/null +++ b/packages/compass-collection/src/components/mock-data-generator-modal/document-count-screen.tsx @@ -0,0 +1,125 @@ +import { + Body, + css, + palette, + spacing, + TextInput, +} from '@mongodb-js/compass-components'; +import React, { useMemo } from 'react'; +import { connect } from 'react-redux'; +import { CollectionState } from '../../modules/collection-tab'; +import { SchemaAnalysisState } from '../../schema-analysis-types'; +import numeral from 'numeral'; +import { DEFAULT_DOCUMENT_COUNT, MAX_DOCUMENT_COUNT } from './constants'; + +const titleStyles = css({ + fontWeight: 600, +}); + +const descriptionStyles = css({ + color: palette.gray.dark1, + fontStyle: 'italic', +}); + +const inputContainerStyles = css({ + display: 'flex', + flexDirection: 'row', + gap: spacing[600], + marginTop: spacing[200], +}); + +const estimatedDiskSizeStyles = css({ + fontSize: '13px', + marginTop: spacing[100], +}); + +const boldStyles = css({ + fontWeight: 600, +}); + +const formatBytes = (bytes: number) => { + const precision = bytes <= 1000 ? '0' : '0.0'; + return numeral(bytes).format(precision + 'b'); +}; + +interface OwnProps { + documentCount: number; + onDocumentCountChange: (documentCount: number) => void; +} + +interface Props extends OwnProps { + schemaAnalysisState: SchemaAnalysisState; +} + +const DocumentCountScreen = ({ + documentCount, + onDocumentCountChange, + schemaAnalysisState, +}: Props) => { + const estimatedDiskSize = useMemo(() => { + return schemaAnalysisState.status === 'complete' + ? schemaAnalysisState.schemaMetadata.avgDocumentSize * documentCount + : 0; + }, [schemaAnalysisState, documentCount]); + + const errorState = useMemo(() => { + return documentCount < 1 || documentCount > MAX_DOCUMENT_COUNT + ? 'error' + : 'none'; + }, [documentCount]); + + const errorMessage = useMemo(() => { + return documentCount < 1 || documentCount > MAX_DOCUMENT_COUNT + ? 'Document count must be between 1 and 100000' + : undefined; + }, [documentCount]); + + return schemaAnalysisState.status === 'complete' ? ( +
+ + Specify Number of Documents to Generate + + + Indicate the amount of documents you want to generate below. +
+ Note: We have defaulted to {DEFAULT_DOCUMENT_COUNT}. + +
+ onDocumentCountChange(Number(e.target.value))} + min={1} + max={MAX_DOCUMENT_COUNT} + state={errorState} + errorMessage={errorMessage} + /> +
+ Estimated Disk Size + + {formatBytes(estimatedDiskSize)} + +
+
+
+ ) : ( + // Not reachable since schema analysis must be finished before the modal can be opened +
We are analyzing your collection.
+ ); +}; + +const mapStateToProps = (state: CollectionState, _ownProps: OwnProps) => { + const schemaAnalysisState = state.schemaAnalysis; + + return { + schemaAnalysisState, + }; +}; + +const ConnectedDocumentCountScreen = connect( + mapStateToProps, + {} +)(DocumentCountScreen); + +export default ConnectedDocumentCountScreen; From 5905c3194b79a06146f7c9d0bd789d4d515400c3 Mon Sep 17 00:00:00 2001 From: Nataly Carbonell Date: Thu, 25 Sep 2025 16:15:44 -0400 Subject: [PATCH 2/4] feat(compass-collection): Document Count Screen - Mock Data Generator CLOUDP-333856 --- packages/compass-collection/package.json | 1 + .../mock-data-generator-modal/constants.ts | 3 + .../document-count-screen.tsx | 74 +++++++++------- .../mock-data-generator-modal.spec.tsx | 85 ++++++++++++++++++- .../mock-data-generator-modal.tsx | 32 ++++++- .../src/modules/collection-tab.ts | 12 ++- .../src/schema-analysis-types.ts | 1 + .../src/stores/collection-tab.ts | 1 + 8 files changed, 172 insertions(+), 37 deletions(-) diff --git a/packages/compass-collection/package.json b/packages/compass-collection/package.json index 8743a12f7e3..ebb3a5a5590 100644 --- a/packages/compass-collection/package.json +++ b/packages/compass-collection/package.json @@ -67,6 +67,7 @@ "mongodb-collection-model": "^5.34.2", "mongodb-ns": "^3.0.1", "mongodb-schema": "^12.6.3", + "numeral": "^2.0.6", "react": "^17.0.2", "react-redux": "^8.1.3", "redux": "^4.2.1", diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/constants.ts b/packages/compass-collection/src/components/mock-data-generator-modal/constants.ts index 942d9f905a0..10c3c707087 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/constants.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/constants.ts @@ -7,3 +7,6 @@ export const StepButtonLabelMap = { [MockDataGeneratorStep.PREVIEW_DATA]: 'Generate Script', [MockDataGeneratorStep.GENERATE_DATA]: 'Done', } as const; + +export const DEFAULT_DOCUMENT_COUNT = 1000; +export const MAX_DOCUMENT_COUNT = 100000; diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/document-count-screen.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/document-count-screen.tsx index 1649b16cd51..4ffa90d318e 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/document-count-screen.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/document-count-screen.tsx @@ -7,8 +7,8 @@ import { } from '@mongodb-js/compass-components'; import React, { useMemo } from 'react'; import { connect } from 'react-redux'; -import { CollectionState } from '../../modules/collection-tab'; -import { SchemaAnalysisState } from '../../schema-analysis-types'; +import type { CollectionState } from '../../modules/collection-tab'; +import type { SchemaAnalysisState } from '../../schema-analysis-types'; import numeral from 'numeral'; import { DEFAULT_DOCUMENT_COUNT, MAX_DOCUMENT_COUNT } from './constants'; @@ -42,6 +42,15 @@ const formatBytes = (bytes: number) => { return numeral(bytes).format(precision + 'b'); }; +type ErrorState = + | { + state: 'error'; + message: string; + } + | { + state: 'none'; + }; + interface OwnProps { documentCount: number; onDocumentCountChange: (documentCount: number) => void; @@ -56,23 +65,30 @@ const DocumentCountScreen = ({ onDocumentCountChange, schemaAnalysisState, }: Props) => { - const estimatedDiskSize = useMemo(() => { - return schemaAnalysisState.status === 'complete' - ? schemaAnalysisState.schemaMetadata.avgDocumentSize * documentCount - : 0; - }, [schemaAnalysisState, documentCount]); - - const errorState = useMemo(() => { - return documentCount < 1 || documentCount > MAX_DOCUMENT_COUNT - ? 'error' - : 'none'; - }, [documentCount]); - - const errorMessage = useMemo(() => { - return documentCount < 1 || documentCount > MAX_DOCUMENT_COUNT - ? 'Document count must be between 1 and 100000' - : undefined; - }, [documentCount]); + const estimatedDiskSize = useMemo( + () => + schemaAnalysisState.status === 'complete' && + schemaAnalysisState.schemaMetadata.avgDocumentSize + ? formatBytes( + schemaAnalysisState.schemaMetadata.avgDocumentSize * documentCount + ) + : 'Not available', + [schemaAnalysisState, documentCount] + ); + + const isOutOfRange = documentCount < 1 || documentCount > MAX_DOCUMENT_COUNT; + + const errorState: ErrorState = useMemo(() => { + if (isOutOfRange) { + return { + state: 'error', + message: `Document count must be between 1 and ${MAX_DOCUMENT_COUNT}`, + }; + } + return { + state: 'none', + }; + }, [isOutOfRange]); return schemaAnalysisState.status === 'complete' ? (
@@ -92,14 +108,14 @@ const DocumentCountScreen = ({ onChange={(e) => onDocumentCountChange(Number(e.target.value))} min={1} max={MAX_DOCUMENT_COUNT} - state={errorState} - errorMessage={errorMessage} + state={errorState.state} + errorMessage={ + errorState.state === 'error' ? errorState.message : undefined + } />
Estimated Disk Size - - {formatBytes(estimatedDiskSize)} - + {estimatedDiskSize}
@@ -109,13 +125,9 @@ const DocumentCountScreen = ({ ); }; -const mapStateToProps = (state: CollectionState, _ownProps: OwnProps) => { - const schemaAnalysisState = state.schemaAnalysis; - - return { - schemaAnalysisState, - }; -}; +const mapStateToProps = (state: CollectionState) => ({ + schemaAnalysisState: state.schemaAnalysis, +}); const ConnectedDocumentCountScreen = connect( mapStateToProps, diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx index e758e4dbac6..14aacd0d586 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx @@ -29,7 +29,11 @@ const defaultSchemaAnalysisState: SchemaAnalysisState = { }, }, sampleDocument: { name: 'John' }, - schemaMetadata: { maxNestingDepth: 1, validationRules: null }, + schemaMetadata: { + maxNestingDepth: 1, + validationRules: null, + avgDocumentSize: undefined, + }, }; describe('MockDataGeneratorModal', () => { @@ -627,6 +631,85 @@ describe('MockDataGeneratorModal', () => { }); }); + describe('on the document count step', () => { + it('displays the correct step title and description', async () => { + await renderModal({ currentStep: MockDataGeneratorStep.DOCUMENT_COUNT }); + + expect(screen.getByText('Specify Number of Documents to Generate')).to + .exist; + + expect( + screen.getByText( + /Indicate the amount of documents you want to generate below./ + ) + ).to.exist; + expect(screen.getByText(/Note: We have defaulted to 1000./)).to.exist; + }); + + it('displays the default document count when the user does not enter a document count', async () => { + await renderModal({ currentStep: MockDataGeneratorStep.DOCUMENT_COUNT }); + + expect( + screen.getByLabelText('Documents to generate in current collection') + ).to.have.value('1000'); + }); + + it('disables the Next button and shows an error message when the document count is greater than 100000', async () => { + await renderModal({ currentStep: MockDataGeneratorStep.DOCUMENT_COUNT }); + + userEvent.type( + screen.getByLabelText('Documents to generate in current collection'), + '100001' + ); + + expect(screen.getByText('Document count must be between 1 and 100000')).to + .exist; + expect( + screen.getByTestId('next-step-button').getAttribute('aria-disabled') + ).to.equal('true'); + }); + + it('displays "Not available" when the avgDocumentSize is undefined', async () => { + await renderModal({ + currentStep: MockDataGeneratorStep.DOCUMENT_COUNT, + schemaAnalysis: { + ...defaultSchemaAnalysisState, + schemaMetadata: { + ...defaultSchemaAnalysisState.schemaMetadata, + avgDocumentSize: undefined, + }, + }, + }); + + expect(screen.getByText('Estimated Disk Size')).to.exist; + expect(screen.getByText('Not available')).to.exist; + }); + + it('displays the correct estimated disk size when a valid document count is entered', async () => { + await renderModal({ + currentStep: MockDataGeneratorStep.DOCUMENT_COUNT, + schemaAnalysis: { + ...defaultSchemaAnalysisState, + schemaMetadata: { + ...defaultSchemaAnalysisState.schemaMetadata, + avgDocumentSize: 100, // 100 bytes + }, + }, + }); + + expect(screen.getByText('Estimated Disk Size')).to.exist; + const documentCountInput = screen.getByLabelText( + 'Documents to generate in current collection' + ); + userEvent.clear(documentCountInput); + userEvent.type(documentCountInput, '1000'); + expect(screen.getByText('100.0KB')).to.exist; + userEvent.clear(documentCountInput); + userEvent.type(documentCountInput, '2000'); + expect(screen.getByText('200.0KB')).to.exist; + }); + }); + describe('on the generate data step', () => { it('enables the Back button', async () => { await renderModal({ currentStep: MockDataGeneratorStep.GENERATE_DATA }); diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.tsx index e2aa91a67a8..82e4bf7ca02 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.tsx @@ -14,7 +14,11 @@ import { } from '@mongodb-js/compass-components'; import { type MockDataGeneratorState, MockDataGeneratorStep } from './types'; -import { StepButtonLabelMap } from './constants'; +import { + DEFAULT_DOCUMENT_COUNT, + MAX_DOCUMENT_COUNT, + StepButtonLabelMap, +} from './constants'; import type { CollectionState } from '../../modules/collection-tab'; import { mockDataGeneratorModalClosed, @@ -25,6 +29,7 @@ import { import RawSchemaConfirmationScreen from './raw-schema-confirmation-screen'; import FakerSchemaEditorScreen from './faker-schema-editor-screen'; import ScriptScreen from './script-screen'; +import DocumentCountScreen from './document-count-screen'; const footerStyles = css` flex-direction: row; @@ -65,6 +70,9 @@ const MockDataGeneratorModal = ({ }: Props) => { const [isSchemaConfirmed, setIsSchemaConfirmed] = React.useState(false); + const [documentCount, setDocumentCount] = React.useState( + DEFAULT_DOCUMENT_COUNT + ); const modalBodyContent = useMemo(() => { switch (currentStep) { @@ -79,16 +87,32 @@ const MockDataGeneratorModal = ({ /> ); case MockDataGeneratorStep.DOCUMENT_COUNT: - return <>; // TODO: CLOUDP-333856 + return ( + + ); case MockDataGeneratorStep.PREVIEW_DATA: return <>; // TODO: CLOUDP-333857 case MockDataGeneratorStep.GENERATE_DATA: return ; } - }, [currentStep, fakerSchemaGenerationState, isSchemaConfirmed]); + }, [ + currentStep, + fakerSchemaGenerationState, + isSchemaConfirmed, + documentCount, + setDocumentCount, + ]); const isNextButtonDisabled = - currentStep === MockDataGeneratorStep.SCHEMA_EDITOR && !isSchemaConfirmed; + (currentStep === MockDataGeneratorStep.SCHEMA_EDITOR && + !isSchemaConfirmed) || + (currentStep === MockDataGeneratorStep.DOCUMENT_COUNT && + documentCount < 1) || + (currentStep === MockDataGeneratorStep.DOCUMENT_COUNT && + documentCount > MAX_DOCUMENT_COUNT); const handleNextClick = () => { if (currentStep === MockDataGeneratorStep.GENERATE_DATA) { diff --git a/packages/compass-collection/src/modules/collection-tab.ts b/packages/compass-collection/src/modules/collection-tab.ts index 77720bd4bf3..8dfddcc636f 100644 --- a/packages/compass-collection/src/modules/collection-tab.ts +++ b/packages/compass-collection/src/modules/collection-tab.ts @@ -33,6 +33,7 @@ import { processSchema, ProcessSchemaUnsupportedStateError, } from '../transform-schema-to-field-info'; +import type { Collection } from '@mongodb-js/compass-app-stores/provider'; import type { Document, MongoError } from 'mongodb'; import { MockDataGeneratorStep } from '../components/mock-data-generator-modal/types'; import type { @@ -94,6 +95,7 @@ type CollectionThunkAction = ThunkAction< connectionInfoRef: ConnectionInfoRef; fakerSchemaGenerationAbortControllerRef: { current?: AbortController }; schemaAnalysisAbortControllerRef: { current?: AbortController }; + collection: Collection; }, A >; @@ -147,6 +149,7 @@ interface SchemaAnalysisFinishedAction { schemaMetadata: { maxNestingDepth: number; validationRules: Document | null; + avgDocumentSize: number | undefined; }; } @@ -561,7 +564,13 @@ export const analyzeCollectionSchema = (): CollectionThunkAction< return async ( dispatch, getState, - { dataService, preferences, logger, schemaAnalysisAbortControllerRef } + { + dataService, + preferences, + logger, + schemaAnalysisAbortControllerRef, + collection: collectionModel, + } ) => { const { schemaAnalysis, namespace } = getState(); const analysisStatus = schemaAnalysis.status; @@ -638,6 +647,7 @@ export const analyzeCollectionSchema = (): CollectionThunkAction< const schemaMetadata = { maxNestingDepth, validationRules, + avgDocumentSize: collectionModel.avg_document_size, }; // Final check before dispatching results diff --git a/packages/compass-collection/src/schema-analysis-types.ts b/packages/compass-collection/src/schema-analysis-types.ts index 954080599af..7e07961b4b0 100644 --- a/packages/compass-collection/src/schema-analysis-types.ts +++ b/packages/compass-collection/src/schema-analysis-types.ts @@ -58,6 +58,7 @@ export type SchemaAnalysisCompletedState = { schemaMetadata: { maxNestingDepth: number; validationRules: Document | null; + avgDocumentSize: number | undefined; }; }; diff --git a/packages/compass-collection/src/stores/collection-tab.ts b/packages/compass-collection/src/stores/collection-tab.ts index b49f4e90845..fc404e20c1f 100644 --- a/packages/compass-collection/src/stores/collection-tab.ts +++ b/packages/compass-collection/src/stores/collection-tab.ts @@ -109,6 +109,7 @@ export function activatePlugin( applyMiddleware( thunk.withExtraArgument({ dataService, + collection: collectionModel, atlasAiService, workspaces, localAppRegistry, From a9b7f372216ba3256ae0ae5cc14db67e314c8c4c Mon Sep 17 00:00:00 2001 From: Nataly Carbonell Date: Thu, 25 Sep 2025 16:25:25 -0400 Subject: [PATCH 3/4] include updated json package --- package-lock.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/package-lock.json b/package-lock.json index 0246bf1938d..00ed4c2be13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48132,6 +48132,7 @@ "mongodb-collection-model": "^5.34.2", "mongodb-ns": "^3.0.1", "mongodb-schema": "^12.6.3", + "numeral": "^2.0.6", "react": "^17.0.2", "react-redux": "^8.1.3", "redux": "^4.2.1", @@ -48182,6 +48183,15 @@ "node": ">=0.3.1" } }, + "packages/compass-collection/node_modules/numeral": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz", + "integrity": "sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "packages/compass-collection/node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -61948,6 +61958,7 @@ "mongodb-collection-model": "^5.34.2", "mongodb-ns": "^3.0.1", "mongodb-schema": "^12.6.3", + "numeral": "^2.0.6", "nyc": "^15.1.0", "react": "^17.0.2", "react-dom": "^17.0.2", @@ -61973,6 +61984,11 @@ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, + "numeral": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz", + "integrity": "sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA==" + }, "semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", From 2569548f014fb31d68a432e088215998f1920862 Mon Sep 17 00:00:00 2001 From: Nataly Carbonell Date: Thu, 25 Sep 2025 16:39:15 -0400 Subject: [PATCH 4/4] improve byte formatting and document count handling --- .../document-count-screen.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/document-count-screen.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/document-count-screen.tsx index 4ffa90d318e..1a44fe569eb 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/document-count-screen.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/document-count-screen.tsx @@ -12,6 +12,8 @@ import type { SchemaAnalysisState } from '../../schema-analysis-types'; import numeral from 'numeral'; import { DEFAULT_DOCUMENT_COUNT, MAX_DOCUMENT_COUNT } from './constants'; +const BYTE_PRECISION_THRESHOLD = 1000; + const titleStyles = css({ fontWeight: 600, }); @@ -38,7 +40,7 @@ const boldStyles = css({ }); const formatBytes = (bytes: number) => { - const precision = bytes <= 1000 ? '0' : '0.0'; + const precision = bytes <= BYTE_PRECISION_THRESHOLD ? '0' : '0.0'; return numeral(bytes).format(precision + 'b'); }; @@ -90,6 +92,15 @@ const DocumentCountScreen = ({ }; }, [isOutOfRange]); + const handleDocumentCountChange = ( + event: React.ChangeEvent + ) => { + const value = parseInt(event.target.value, 10); + if (!isNaN(value)) { + onDocumentCountChange(value); + } + }; + return schemaAnalysisState.status === 'complete' ? (
@@ -105,7 +116,7 @@ const DocumentCountScreen = ({ label="Documents to generate in current collection" type="number" value={documentCount.toString()} - onChange={(e) => onDocumentCountChange(Number(e.target.value))} + onChange={handleDocumentCountChange} min={1} max={MAX_DOCUMENT_COUNT} state={errorState.state}