Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/compass-collection/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
@@ -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';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: instead of adding a dependency I would suggest to move the formatting helpers in https://github.com/mongodb-js/compass/blob/main/packages/databases-collections-list/src/format.ts to compass-components package, and use them here, this way we will have consistent formatting across the code

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<HTMLInputElement>
) => {
const value = parseInt(event.target.value, 10);
if (!isNaN(value)) {
onDocumentCountChange(value);
}
};

return schemaAnalysisState.status === 'complete' ? (
Comment on lines +103 to +104
Copy link
Collaborator

@gribnoysup gribnoysup Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super duper nit: for readability it might be easier to short circuit here first, then return the form instead of using a ternary

Suggested change
return schemaAnalysisState.status === 'complete' ? (
if (schemaAnalysisState.status !== 'complete') {
return <div>We are analyzing your collection.</div>
}
// ... rest of the form

<div>
<Body className={titleStyles}>
Specify Number of Documents to Generate
</Body>
<Body className={descriptionStyles}>
Indicate the amount of documents you want to generate below.
<br />
Note: We have defaulted to {DEFAULT_DOCUMENT_COUNT}.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super small nit: I know it's in the designs, but this "Note" here feels redundant, we already prefill the input with the default count, so doesn't seem to be a reason to spell it

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree fully, was going to flag this actually

</Body>
<div className={inputContainerStyles}>
<TextInput
label="Documents to generate in current collection"
type="number"
value={documentCount.toString()}
onChange={handleDocumentCountChange}
min={1}
max={MAX_DOCUMENT_COUNT}
state={errorState.state}
errorMessage={
errorState.state === 'error' ? errorState.message : undefined
}
/>
<div>
<Body className={boldStyles}>Estimated Disk Size</Body>
<Body className={estimatedDiskSizeStyles}>{estimatedDiskSize}</Body>
</div>
</div>
</div>
) : (
// Not reachable since schema analysis must be finished before the modal can be opened
<div>We are analyzing your collection.</div>
);
};

const mapStateToProps = (state: CollectionState) => ({
schemaAnalysisState: state.schemaAnalysis,
});

const ConnectedDocumentCountScreen = connect(
mapStateToProps,
{}
)(DocumentCountScreen);

export default ConnectedDocumentCountScreen;
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ const defaultSchemaAnalysisState: SchemaAnalysisState = {
},
},
sampleDocument: { name: 'John' },
schemaMetadata: { maxNestingDepth: 1, validationRules: null },
schemaMetadata: {
maxNestingDepth: 1,
validationRules: null,
avgDocumentSize: undefined,
},
};

describe('MockDataGeneratorModal', () => {
Expand Down Expand Up @@ -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 });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -65,6 +70,9 @@ const MockDataGeneratorModal = ({
}: Props) => {
const [isSchemaConfirmed, setIsSchemaConfirmed] =
React.useState<boolean>(false);
const [documentCount, setDocumentCount] = React.useState<number>(
DEFAULT_DOCUMENT_COUNT
);

const modalBodyContent = useMemo(() => {
switch (currentStep) {
Expand All @@ -79,16 +87,32 @@ const MockDataGeneratorModal = ({
/>
);
case MockDataGeneratorStep.DOCUMENT_COUNT:
return <></>; // TODO: CLOUDP-333856
return (
<DocumentCountScreen
documentCount={documentCount}
onDocumentCountChange={setDocumentCount}
/>
);
case MockDataGeneratorStep.PREVIEW_DATA:
return <></>; // TODO: CLOUDP-333857
case MockDataGeneratorStep.GENERATE_DATA:
return <ScriptScreen />;
}
}, [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) {
Expand Down
Loading
Loading