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", 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 new file mode 100644 index 00000000000..1a44fe569eb --- /dev/null +++ b/packages/compass-collection/src/components/mock-data-generator-modal/document-count-screen.tsx @@ -0,0 +1,148 @@ +import { + Body, + css, + palette, + spacing, + TextInput, +} from '@mongodb-js/compass-components'; +import React, { useMemo } from 'react'; +import { connect } from 'react-redux'; +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'; + +const BYTE_PRECISION_THRESHOLD = 1000; + +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 <= BYTE_PRECISION_THRESHOLD ? '0' : '0.0'; + return numeral(bytes).format(precision + 'b'); +}; + +type ErrorState = + | { + state: 'error'; + message: string; + } + | { + state: 'none'; + }; + +interface OwnProps { + documentCount: number; + onDocumentCountChange: (documentCount: number) => void; +} + +interface Props extends OwnProps { + schemaAnalysisState: SchemaAnalysisState; +} + +const DocumentCountScreen = ({ + documentCount, + onDocumentCountChange, + schemaAnalysisState, +}: Props) => { + 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]); + + const handleDocumentCountChange = ( + event: React.ChangeEvent + ) => { + const value = parseInt(event.target.value, 10); + if (!isNaN(value)) { + onDocumentCountChange(value); + } + }; + + 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}. + +
+ +
+ Estimated Disk Size + {estimatedDiskSize} +
+
+
+ ) : ( + // Not reachable since schema analysis must be finished before the modal can be opened +
We are analyzing your collection.
+ ); +}; + +const mapStateToProps = (state: CollectionState) => ({ + schemaAnalysisState: state.schemaAnalysis, +}); + +const ConnectedDocumentCountScreen = connect( + mapStateToProps, + {} +)(DocumentCountScreen); + +export default ConnectedDocumentCountScreen; 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,